modules/controls/src/main/java/javafx/scene/control/skin/TabPaneSkin.java

Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization

@@ -21,12 +21,14 @@
  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
 
-package com.sun.javafx.scene.control.skin;
+package javafx.scene.control.skin;
 
+import com.sun.javafx.scene.control.LambdaMultiplePropertyChangeListenerHandler;
+import com.sun.javafx.scene.control.Properties;
 import com.sun.javafx.util.Utils;
 import javafx.animation.Animation;
 import javafx.animation.Interpolator;
 import javafx.animation.KeyFrame;
 import javafx.animation.KeyValue;

@@ -56,10 +58,11 @@
 import javafx.scene.AccessibleAction;
 import javafx.scene.AccessibleAttribute;
 import javafx.scene.AccessibleRole;
 import javafx.scene.Node;
 import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Control;
 import javafx.scene.control.Label;
 import javafx.scene.control.MenuItem;
 import javafx.scene.control.RadioMenuItem;
 import javafx.scene.control.SkinBase;
 import javafx.scene.control.Tab;

@@ -84,107 +87,96 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 
-import com.sun.javafx.css.converters.EnumConverter;
-import com.sun.javafx.scene.control.MultiplePropertyChangeListenerHandler;
+import javafx.css.converter.EnumConverter;
 import com.sun.javafx.scene.control.behavior.TabPaneBehavior;
 import com.sun.javafx.scene.traversal.Direction;
 import com.sun.javafx.scene.traversal.TraversalEngine;
 
 import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString;
 
-public class TabPaneSkin extends BehaviorSkinBase<TabPane, TabPaneBehavior> {
-    private static enum TabAnimation {
+/**
+ * Default skin implementation for the {@link TabPane} control.
+ *
+ * @see TabPane
+ * @since 9
+ */
+public class TabPaneSkin extends SkinBase<TabPane> {
+
+    /***************************************************************************
+     *                                                                         *
+     * Enums                                                                   *
+     *                                                                         *
+     **************************************************************************/
+
+    private enum TabAnimation {
         NONE,
         GROW
         // In future we could add FADE, ...
     }
 
     private enum TabAnimationState {
         SHOWING, HIDING, NONE;
     }
     
-    private ObjectProperty<TabAnimation> openTabAnimation = new StyleableObjectProperty<TabAnimation>(TabAnimation.GROW) {
-        @Override public CssMetaData<TabPane,TabAnimation> getCssMetaData() {
-            return StyleableProperties.OPEN_TAB_ANIMATION;
-        }
         
-        @Override public Object getBean() {
-            return TabPaneSkin.this;
-        }
 
-        @Override public String getName() {
-            return "openTabAnimation";
-        }
-    };
+    /***************************************************************************
+     *                                                                         *
+     * Static fields                                                           *
+     *                                                                         *
+     **************************************************************************/
     
-    private ObjectProperty<TabAnimation> closeTabAnimation = new StyleableObjectProperty<TabAnimation>(TabAnimation.GROW) {
-        @Override public CssMetaData<TabPane,TabAnimation> getCssMetaData() {
-            return StyleableProperties.CLOSE_TAB_ANIMATION;
-        }
+    static int CLOSE_BTN_SIZE = 16;
 
-        @Override public Object getBean() {
-            return TabPaneSkin.this;
-        }
 
-        @Override public String getName() {
-            return "closeTabAnimation";
-        }
-    };
     
-    private static int getRotation(Side pos) {
-        switch (pos) {
-            case TOP:
-                return 0;
-            case BOTTOM:
-                return 180;
-            case LEFT:
-                return -90;
-            case RIGHT:
-                return 90;
-            default:
-                return 0;
-        }
-    }
+    /***************************************************************************
+     *                                                                         *
+     * Private fields                                                          *
+     *                                                                         *
+     **************************************************************************/
 
-    /**
-     * VERY HACKY - this lets us 'duplicate' Label and ImageView nodes to be used in a
-     * Tab and the tabs menu at the same time.
-     */
-    private static Node clone(Node n) {
-        if (n == null) {
-            return null;
-        }
-        if (n instanceof ImageView) {
-            ImageView iv = (ImageView) n;
-            ImageView imageview = new ImageView();
-            imageview.setImage(iv.getImage());
-            return imageview;
-        }
-        if (n instanceof Label) {            
-            Label l = (Label)n;
-            Label label = new Label(l.getText(), l.getGraphic());
-            return label;
-        }
-        return null;
-    }
     private static final double ANIMATION_SPEED = 150;
     private static final int SPACER = 10;
 
     private TabHeaderArea tabHeaderArea;
     private ObservableList<TabContentRegion> tabContentRegions;
     private Rectangle clipRect;
     private Rectangle tabHeaderAreaClipRect;
     private Tab selectedTab;
     private boolean isSelectingTab;
+    private double maxw = 0.0d;
+    private double maxh = 0.0d;
+
+    private final TabPaneBehavior behavior;
+
+
 
-    public TabPaneSkin(TabPane tabPane) {
-        super(tabPane, new TabPaneBehavior(tabPane));
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Creates a new TabPaneSkin instance, installing the necessary child
+     * nodes into the Control {@link Control#getChildren() children} list, as
+     * well as the necessary input mappings for handling key, mouse, etc events.
+     *
+     * @param control The control that this skin should be installed onto.
+     */
+    public TabPaneSkin(TabPane control) {
+        super(control);
 
-        clipRect = new Rectangle(tabPane.getWidth(), tabPane.getHeight());
+        // install default input map for the TabPane control
+        this.behavior = new TabPaneBehavior(control);
+//        control.setInputMap(behavior.getInputMap());
+
+        clipRect = new Rectangle(control.getWidth(), control.getHeight());
         getSkinnable().setClip(clipRect);
 
         tabContentRegions = FXCollections.<TabContentRegion>observableArrayList();
 
         for (Tab tab : getSkinnable().getTabs()) {

@@ -199,14 +191,18 @@
             tabHeaderArea.setVisible(false);
         }
 
         initializeTabListener();
 
-        registerChangeListener(tabPane.getSelectionModel().selectedItemProperty(), "SELECTED_TAB");
-        registerChangeListener(tabPane.sideProperty(), "SIDE");
-        registerChangeListener(tabPane.widthProperty(), "WIDTH");
-        registerChangeListener(tabPane.heightProperty(), "HEIGHT");
+        registerChangeListener(control.getSelectionModel().selectedItemProperty(), e -> {
+            isSelectingTab = true;
+            selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
+            getSkinnable().requestLayout();
+        });
+        registerChangeListener(control.sideProperty(), e -> updateTabPosition());
+        registerChangeListener(control.widthProperty(), e -> clipRect.setWidth(getSkinnable().getWidth()));
+        registerChangeListener(control.heightProperty(), e -> clipRect.setHeight(getSkinnable().getHeight()));
 
         selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
         // Could not find the selected tab try and get the selected tab using the selected index
         if (selectedTab == null && getSkinnable().getSelectionModel().getSelectedIndex() != -1) {
             getSkinnable().getSelectionModel().select(getSkinnable().getSelectionModel().getSelectedIndex());

@@ -219,34 +215,247 @@
         selectedTab = getSkinnable().getSelectionModel().getSelectedItem();        
         isSelectingTab = false;
 
         initializeSwipeHandlers();
     }
-
-    public StackPane getSelectedTabContentRegion() {
-        for (TabContentRegion contentRegion : tabContentRegions) {
-            if (contentRegion.getTab().equals(selectedTab)) {
-                return contentRegion;
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Properties                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    private ObjectProperty<TabAnimation> openTabAnimation = new StyleableObjectProperty<TabAnimation>(TabAnimation.GROW) {
+        @Override public CssMetaData<TabPane,TabAnimation> getCssMetaData() {
+            return StyleableProperties.OPEN_TAB_ANIMATION;
+        }
+
+        @Override public Object getBean() {
+            return TabPaneSkin.this;
+        }
+
+        @Override public String getName() {
+            return "openTabAnimation";
+        }
+    };
+
+    private ObjectProperty<TabAnimation> closeTabAnimation = new StyleableObjectProperty<TabAnimation>(TabAnimation.GROW) {
+        @Override public CssMetaData<TabPane,TabAnimation> getCssMetaData() {
+            return StyleableProperties.CLOSE_TAB_ANIMATION;
+        }
+
+        @Override public Object getBean() {
+            return TabPaneSkin.this;
+        }
+
+        @Override public String getName() {
+            return "closeTabAnimation";
+        }
+    };
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Public API                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    /** {@inheritDoc} */
+    @Override public void dispose() {
+        super.dispose();
+
+        if (behavior != null) {
+            behavior.dispose();
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        // The TabPane can only be as wide as it widest content width.
+        for (TabContentRegion contentRegion: tabContentRegions) {
+            maxw = Math.max(maxw, snapSize(contentRegion.prefWidth(-1)));
+        }
+
+        final boolean isHorizontal = isHorizontal();
+        final double tabHeaderAreaSize = snapSize(isHorizontal ?
+                tabHeaderArea.prefWidth(-1) : tabHeaderArea.prefHeight(-1));
+
+        double prefWidth = isHorizontal ?
+                Math.max(maxw, tabHeaderAreaSize) : maxw + tabHeaderAreaSize;
+        return snapSize(prefWidth) + rightInset + leftInset;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        // The TabPane can only be as high as it highest content height.
+        for (TabContentRegion contentRegion: tabContentRegions) {
+            maxh = Math.max(maxh, snapSize(contentRegion.prefHeight(-1)));
+        }
+
+        final boolean isHorizontal = isHorizontal();
+        final double tabHeaderAreaSize = snapSize(isHorizontal ?
+                tabHeaderArea.prefHeight(-1) : tabHeaderArea.prefWidth(-1));
+
+        double prefHeight = isHorizontal ?
+                maxh + snapSize(tabHeaderAreaSize) : Math.max(maxh, tabHeaderAreaSize);
+        return snapSize(prefHeight) + topInset + bottomInset;
+    }
+
+    /** {@inheritDoc} */
+    @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
+        Side tabPosition = getSkinnable().getSide();
+        if (tabPosition == Side.TOP) {
+            return tabHeaderArea.getBaselineOffset() + topInset;
+        }
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void layoutChildren(final double x, final double y,
+                                            final double w, final double h) {
+        TabPane tabPane = getSkinnable();
+        Side tabPosition = tabPane.getSide();
+
+        double headerHeight = snapSize(tabHeaderArea.prefHeight(-1));
+        double tabsStartX = tabPosition.equals(Side.RIGHT)? x + w - headerHeight : x;
+        double tabsStartY = tabPosition.equals(Side.BOTTOM)? y + h - headerHeight : y;
+
+        if (tabPosition == Side.TOP) {
+            tabHeaderArea.resize(w, headerHeight);
+            tabHeaderArea.relocate(tabsStartX, tabsStartY);
+            tabHeaderArea.getTransforms().clear();
+            tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.TOP)));
+        } else if (tabPosition == Side.BOTTOM) {
+            tabHeaderArea.resize(w, headerHeight);
+            tabHeaderArea.relocate(w, tabsStartY - headerHeight);
+            tabHeaderArea.getTransforms().clear();
+            tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.BOTTOM), 0, headerHeight));
+        } else if (tabPosition == Side.LEFT) {
+            tabHeaderArea.resize(h, headerHeight);
+            tabHeaderArea.relocate(tabsStartX + headerHeight, h - headerHeight);
+            tabHeaderArea.getTransforms().clear();
+            tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.LEFT), 0, headerHeight));
+        } else if (tabPosition == Side.RIGHT) {
+            tabHeaderArea.resize(h, headerHeight);
+            tabHeaderArea.relocate(tabsStartX, y - headerHeight);
+            tabHeaderArea.getTransforms().clear();
+            tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.RIGHT), 0, headerHeight));
+        }
+
+        tabHeaderAreaClipRect.setX(0);
+        tabHeaderAreaClipRect.setY(0);
+        if (isHorizontal()) {
+            tabHeaderAreaClipRect.setWidth(w);
+        } else {
+            tabHeaderAreaClipRect.setWidth(h);
+        }
+        tabHeaderAreaClipRect.setHeight(headerHeight);
+
+        // ==================================
+        // position the tab content for the selected tab only
+        // ==================================
+        // if the tabs are on the left, the content needs to be indented
+        double contentStartX = 0;
+        double contentStartY = 0;
+
+        if (tabPosition == Side.TOP) {
+            contentStartX = x;
+            contentStartY = y + headerHeight;
+            if (isFloatingStyleClass()) {
+                // This is to hide the top border content
+                contentStartY -= 1;
+            }
+        } else if (tabPosition == Side.BOTTOM) {
+            contentStartX = x;
+            contentStartY = y;
+            if (isFloatingStyleClass()) {
+                // This is to hide the bottom border content
+                contentStartY = 1;
+            }
+        } else if (tabPosition == Side.LEFT) {
+            contentStartX = x + headerHeight;
+            contentStartY = y;
+            if (isFloatingStyleClass()) {
+                // This is to hide the left border content
+                contentStartX -= 1;
+            }
+        } else if (tabPosition == Side.RIGHT) {
+            contentStartX = x;
+            contentStartY = y;
+            if (isFloatingStyleClass()) {
+                // This is to hide the right border content
+                contentStartX = 1;
+            }
+        }
+
+        double contentWidth = w - (isHorizontal() ? 0 : headerHeight);
+        double contentHeight = h - (isHorizontal() ? headerHeight: 0);
+
+        for (int i = 0, max = tabContentRegions.size(); i < max; i++) {
+            TabContentRegion tabContent = tabContentRegions.get(i);
+
+            tabContent.setAlignment(Pos.TOP_LEFT);
+            if (tabContent.getClip() != null) {
+                ((Rectangle)tabContent.getClip()).setWidth(contentWidth);
+                ((Rectangle)tabContent.getClip()).setHeight(contentHeight);
+            }
+
+            // we need to size all tabs, even if they aren't visible. For example,
+            // see RT-29167
+            tabContent.resize(contentWidth, contentHeight);
+            tabContent.relocate(contentStartX, contentStartY);
+        }
+    }
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Private implementation                                                  *
+     *                                                                         *
+     **************************************************************************/
+
+    private static int getRotation(Side pos) {
+        switch (pos) {
+            case TOP:
+                return 0;
+            case BOTTOM:
+                return 180;
+            case LEFT:
+                return -90;
+            case RIGHT:
+                return 90;
+            default:
+                return 0;
+        }
+    }
+
+    /**
+     * VERY HACKY - this lets us 'duplicate' Label and ImageView nodes to be used in a
+     * Tab and the tabs menu at the same time.
+     */
+    private static Node clone(Node n) {
+        if (n == null) {
+            return null;
+        }
+        if (n instanceof ImageView) {
+            ImageView iv = (ImageView) n;
+            ImageView imageview = new ImageView();
+            imageview.setImage(iv.getImage());
+            return imageview;
             }
+        if (n instanceof Label) {
+            Label l = (Label)n;
+            Label label = new Label(l.getText(), l.getGraphic());
+            return label;
         }
         return null;
     }
 
-    @Override protected void handleControlPropertyChanged(String property) {
-        super.handleControlPropertyChanged(property);
-        if ("SELECTED_TAB".equals(property)) {            
-            isSelectingTab = true;
-            selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
-            getSkinnable().requestLayout();
-        } else if ("SIDE".equals(property)) {
-            updateTabPosition();
-        } else if ("WIDTH".equals(property)) {
-            clipRect.setWidth(getSkinnable().getWidth());
-        } else if ("HEIGHT".equals(property)) {
-            clipRect.setHeight(getSkinnable().getHeight());
-        }
-    }
     private void removeTabs(List<? extends Tab> removedList) {
         for (final Tab tab : removedList) {
             stopCurrentAnimation(tab);
             // Animate the tab removal
             final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab);

@@ -401,11 +610,11 @@
             // now only remove the tabs that are not in the tabsToAdd list
             tabsToRemove.removeAll(tabsToAdd);
             removeTabs(tabsToRemove);
 
             // and add in any new tabs (that we don't already have showing)
-            if (! tabsToAdd.isEmpty()) {
+            if (!tabsToAdd.isEmpty()) {
                 for (TabContentRegion tabContentRegion : tabContentRegions) {
                     Tab tab = tabContentRegion.getTab();
                     TabHeaderSkin tabHeader = tabHeaderArea.getTabHeaderSkin(tab);
                     if (!tabHeader.isClosing && tabsToAdd.contains(tabContentRegion.getTab())) {
                         tabsToAdd.remove(tabContentRegion.getTab());

@@ -461,162 +670,33 @@
         Side tabPosition = getSkinnable().getSide();
         return Side.TOP.equals(tabPosition) || Side.BOTTOM.equals(tabPosition);
     }
 
     private void initializeSwipeHandlers() {
-        if (IS_TOUCH_SUPPORTED) {
+        if (Properties.IS_TOUCH_SUPPORTED) {
             getSkinnable().addEventHandler(SwipeEvent.SWIPE_LEFT, t -> {
-                getBehavior().selectNextTab();
+                behavior.selectNextTab();
             });
 
             getSkinnable().addEventHandler(SwipeEvent.SWIPE_RIGHT, t -> {
-                getBehavior().selectPreviousTab();
+                behavior.selectPreviousTab();
             });
         }    
     }
     
     //TODO need to cache this.
     private boolean isFloatingStyleClass() {
         return getSkinnable().getStyleClass().contains(TabPane.STYLE_CLASS_FLOATING);
     }
 
-    private double maxw = 0.0d;
-    @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
-        // The TabPane can only be as wide as it widest content width.
-        for (TabContentRegion contentRegion: tabContentRegions) {
-             maxw = Math.max(maxw, snapSize(contentRegion.prefWidth(-1)));
-        }
-
-        final boolean isHorizontal = isHorizontal();
-        final double tabHeaderAreaSize = snapSize(isHorizontal ?
-                tabHeaderArea.prefWidth(-1) : tabHeaderArea.prefHeight(-1));
-
-        double prefWidth = isHorizontal ?
-            Math.max(maxw, tabHeaderAreaSize) : maxw + tabHeaderAreaSize;
-        return snapSize(prefWidth) + rightInset + leftInset;
-    }
-
-    private double maxh = 0.0d;
-    @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
-        // The TabPane can only be as high as it highest content height.
-        for (TabContentRegion contentRegion: tabContentRegions) {
-             maxh = Math.max(maxh, snapSize(contentRegion.prefHeight(-1)));
-        }
-
-        final boolean isHorizontal = isHorizontal();
-        final double tabHeaderAreaSize = snapSize(isHorizontal ?
-                tabHeaderArea.prefHeight(-1) : tabHeaderArea.prefWidth(-1));
-
-        double prefHeight = isHorizontal ?
-             maxh + snapSize(tabHeaderAreaSize) : Math.max(maxh, tabHeaderAreaSize);
-        return snapSize(prefHeight) + topInset + bottomInset;
-    }
-
-    @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
-        Side tabPosition = getSkinnable().getSide();
-        if (tabPosition == Side.TOP) {
-            return tabHeaderArea.getBaselineOffset() + topInset;
-        }
-        return 0;
-    }
-
-    @Override protected void layoutChildren(final double x, final double y,
-            final double w, final double h) {
-        TabPane tabPane = getSkinnable();
-        Side tabPosition = tabPane.getSide();
-
-        double headerHeight = snapSize(tabHeaderArea.prefHeight(-1));
-        double tabsStartX = tabPosition.equals(Side.RIGHT)? x + w - headerHeight : x;
-        double tabsStartY = tabPosition.equals(Side.BOTTOM)? y + h - headerHeight : y;
-
-        if (tabPosition == Side.TOP) {
-            tabHeaderArea.resize(w, headerHeight);
-            tabHeaderArea.relocate(tabsStartX, tabsStartY);
-            tabHeaderArea.getTransforms().clear();
-            tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.TOP)));
-        } else if (tabPosition == Side.BOTTOM) {
-            tabHeaderArea.resize(w, headerHeight);
-            tabHeaderArea.relocate(w, tabsStartY - headerHeight);
-            tabHeaderArea.getTransforms().clear();
-            tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.BOTTOM), 0, headerHeight));
-        } else if (tabPosition == Side.LEFT) {
-            tabHeaderArea.resize(h, headerHeight);
-            tabHeaderArea.relocate(tabsStartX + headerHeight, h - headerHeight);
-            tabHeaderArea.getTransforms().clear();
-            tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.LEFT), 0, headerHeight));
-        } else if (tabPosition == Side.RIGHT) {
-            tabHeaderArea.resize(h, headerHeight);
-            tabHeaderArea.relocate(tabsStartX, y - headerHeight);
-            tabHeaderArea.getTransforms().clear();
-            tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.RIGHT), 0, headerHeight));
-        }
-
-        tabHeaderAreaClipRect.setX(0);
-        tabHeaderAreaClipRect.setY(0);
-        if (isHorizontal()) {
-            tabHeaderAreaClipRect.setWidth(w);
-        } else {
-            tabHeaderAreaClipRect.setWidth(h);
-        }
-        tabHeaderAreaClipRect.setHeight(headerHeight);
-
-        // ==================================
-        // position the tab content for the selected tab only
-        // ==================================
-        // if the tabs are on the left, the content needs to be indented
-        double contentStartX = 0;
-        double contentStartY = 0;
-
-        if (tabPosition == Side.TOP) {
-            contentStartX = x;
-            contentStartY = y + headerHeight;
-            if (isFloatingStyleClass()) {
-                // This is to hide the top border content
-                contentStartY -= 1;
-            }
-        } else if (tabPosition == Side.BOTTOM) {
-            contentStartX = x;
-            contentStartY = y;
-            if (isFloatingStyleClass()) {
-                // This is to hide the bottom border content
-                contentStartY = 1;
-            }
-        } else if (tabPosition == Side.LEFT) {
-            contentStartX = x + headerHeight;
-            contentStartY = y;
-            if (isFloatingStyleClass()) {
-                // This is to hide the left border content
-                contentStartX -= 1;
-            }
-        } else if (tabPosition == Side.RIGHT) {
-            contentStartX = x;
-            contentStartY = y;
-            if (isFloatingStyleClass()) {
-                // This is to hide the right border content
-                contentStartX = 1;
-            }
-        }
-
-        double contentWidth = w - (isHorizontal() ? 0 : headerHeight);
-        double contentHeight = h - (isHorizontal() ? headerHeight: 0);
-        
-        for (int i = 0, max = tabContentRegions.size(); i < max; i++) {
-            TabContentRegion tabContent = tabContentRegions.get(i);
-            
-            tabContent.setAlignment(Pos.TOP_LEFT);
-            if (tabContent.getClip() != null) {
-                ((Rectangle)tabContent.getClip()).setWidth(contentWidth);
-                ((Rectangle)tabContent.getClip()).setHeight(contentHeight);
-            }
             
-            // we need to size all tabs, even if they aren't visible. For example,
-            // see RT-29167
-            tabContent.resize(contentWidth, contentHeight);
-            tabContent.relocate(contentStartX, contentStartY);
-        }
-    }
     
+    /***************************************************************************
+     *                                                                         *
+     * CSS                                                                     *
+     *                                                                         *
+     **************************************************************************/
     
    /**
     * Super-lazy instantiation pattern from Bill Pugh.
     * @treatAsPrivate implementation detail
     */

@@ -661,11 +741,11 @@
 
         }
     }
     
     /**
-     * @return The CssMetaData associated with this class, which may include the
+     * Returns the CssMetaData associated with this class, which may include the
      * CssMetaData of its super classes.
      */
     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
         return StyleableProperties.STYLEABLES;
     }

@@ -675,10 +755,18 @@
      */
     @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
         return getClassCssMetaData();
     }
 
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Support classes                                                         *
+     *                                                                         *
+     **************************************************************************/
+
     /**************************************************************************
      *
      * TabHeaderArea: Area responsible for painting all tabs
      *
      **************************************************************************/

@@ -1093,11 +1181,12 @@
             positionInArea(controlButtons, controlStartX, controlStartY, btnWidth, btnHeight,
                         /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
         }
     } /* End TabHeaderArea */
 
-    static int CLOSE_BTN_SIZE = 16;
+
+
 
     /**************************************************************************
      *
      * TabHeaderSkin: skin for each tab
      *

@@ -1115,15 +1204,11 @@
         private Tooltip tooltip;
         private Rectangle clip;
 
         private boolean isClosing = false;
 
-        private MultiplePropertyChangeListenerHandler listener =
-                new MultiplePropertyChangeListenerHandler(param -> {
-                    handlePropertyChanged(param);
-                    return null;
-                });
+        private LambdaMultiplePropertyChangeListenerHandler listener = new LambdaMultiplePropertyChangeListenerHandler();
         
         private final ListChangeListener<String> styleClassListener = new ListChangeListener<String>() {
             @Override
             public void onChanged(Change<? extends String> c) {
                 getStyleClass().setAll(tab.getStyleClass());

@@ -1156,11 +1241,10 @@
                 @Override
                 public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
                     switch (action) {
                         case FIRE: {
                             Tab tab = getTab();
-                            TabPaneBehavior behavior = getBehavior();
                             if (behavior.canCloseTab(tab)) {
                                 behavior.closeTab(tab);
                                 setOnMousePressed(null);
                             }
                         }

@@ -1170,13 +1254,13 @@
             };
             closeBtn.setAccessibleRole(AccessibleRole.BUTTON);
             closeBtn.setAccessibleText(getString("Accessibility.title.TabPane.CloseButton"));
             closeBtn.getStyleClass().setAll("tab-close-button");
             closeBtn.setOnMousePressed(new EventHandler<MouseEvent>() {
-                @Override public void handle(MouseEvent me) {
+                @Override
+                public void handle(MouseEvent me) {
                     Tab tab = getTab();
-                    TabPaneBehavior behavior = getBehavior();
                     if (behavior.canCloseTab(tab)) {
                          behavior.closeTab(tab);
                          setOnMousePressed(null);    
                     }
                 }

@@ -1284,134 +1368,114 @@
             if (tooltip != null) {
                 Tooltip.install(this, tooltip);
                 oldTooltip = tooltip;
             }
 
-            listener.registerChangeListener(tab.closableProperty(), "CLOSABLE");
-            listener.registerChangeListener(tab.selectedProperty(), "SELECTED");
-            listener.registerChangeListener(tab.textProperty(), "TEXT");
-            listener.registerChangeListener(tab.graphicProperty(), "GRAPHIC");
-            listener.registerChangeListener(tab.contextMenuProperty(), "CONTEXT_MENU");
-            listener.registerChangeListener(tab.tooltipProperty(), "TOOLTIP");
-            listener.registerChangeListener(tab.disableProperty(), "DISABLE");
-            listener.registerChangeListener(tab.styleProperty(), "STYLE");
-            
-            tab.getStyleClass().addListener(weakStyleClassListener);
-
-            listener.registerChangeListener(getSkinnable().tabClosingPolicyProperty(), "TAB_CLOSING_POLICY");
-            listener.registerChangeListener(getSkinnable().sideProperty(), "SIDE");
-            listener.registerChangeListener(getSkinnable().rotateGraphicProperty(), "ROTATE_GRAPHIC");
-            listener.registerChangeListener(getSkinnable().tabMinWidthProperty(), "TAB_MIN_WIDTH");
-            listener.registerChangeListener(getSkinnable().tabMaxWidthProperty(), "TAB_MAX_WIDTH");
-            listener.registerChangeListener(getSkinnable().tabMinHeightProperty(), "TAB_MIN_HEIGHT");
-            listener.registerChangeListener(getSkinnable().tabMaxHeightProperty(), "TAB_MAX_HEIGHT");
-            
-            getProperties().put(Tab.class, tab);
-            getProperties().put(ContextMenu.class, tab.getContextMenu());
-
-            setOnContextMenuRequested((ContextMenuEvent me) -> {
-               if (getTab().getContextMenu() != null) {
-                    getTab().getContextMenu().show(inner, me.getScreenX(), me.getScreenY());
-                    me.consume();
-                }
-            });
-            setOnMousePressed(new EventHandler<MouseEvent>() {
-                @Override public void handle(MouseEvent me) {
-                    if (getTab().isDisable()) {
-                        return;
-                    }
-                    if (me.getButton().equals(MouseButton.MIDDLE)) {
-                        if (showCloseButton()) {
-                            Tab tab = getTab();
-                            TabPaneBehavior behavior = getBehavior();
-                            if (behavior.canCloseTab(tab)) {
-                                removeListeners(tab);
-                                behavior.closeTab(tab);    
-                            }
-                        }
-                    } else if (me.getButton().equals(MouseButton.PRIMARY)) {
-                        getBehavior().selectTab(getTab());
-                    }
-                }
-            });    
-
-            // initialize pseudo-class state
-            pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
-            pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisable());
-            final Side side = getSkinnable().getSide();
-            pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
-            pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
-            pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
-            pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
-        }
-        
-        private void handlePropertyChanged(final String p) {
-            // --- Tab properties
-            if ("CLOSABLE".equals(p)) {
+            listener.registerChangeListener(tab.closableProperty(), e -> {
                 inner.requestLayout();
                 requestLayout();
-            } else if ("SELECTED".equals(p)) {
+            });
+            listener.registerChangeListener(tab.selectedProperty(), e -> {
                 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
                 // Need to request a layout pass for inner because if the width
                 // and height didn't not change the label or close button may have
                 // changed.
                 inner.requestLayout();
                 requestLayout();
-            } else if ("TEXT".equals(p)) {
-                label.setText(getTab().getText());
-            } else if ("GRAPHIC".equals(p)) {
-                label.setGraphic(getTab().getGraphic());
-            } else if ("CONTEXT_MENU".equals(p)) {
-                // todo
-            } else if ("TOOLTIP".equals(p)) {
+            });
+            listener.registerChangeListener(tab.textProperty(),e -> label.setText(getTab().getText()));
+            listener.registerChangeListener(tab.graphicProperty(), e -> label.setGraphic(getTab().getGraphic()));
+            listener.registerChangeListener(tab.tooltipProperty(), e -> {
                 // uninstall the old tooltip
                 if (oldTooltip != null) {
                     Tooltip.uninstall(this, oldTooltip);
                 }
                 tooltip = tab.getTooltip();
                 if (tooltip != null) {
                     // install new tooltip and save as old tooltip.
                     Tooltip.install(this, tooltip);
                     oldTooltip = tooltip; 
                 }
-            } else if ("DISABLE".equals(p)) {
+            });
+            listener.registerChangeListener(tab.disableProperty(), e -> {
                 pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisable());
                 inner.requestLayout();
                 requestLayout();
-            } else if ("STYLE".equals(p)) {
-                setStyle(tab.getStyle());
-            }
+            });
+            listener.registerChangeListener(tab.styleProperty(), e -> setStyle(tab.getStyle()));
             
-            // --- Skinnable properties
-            else if ("TAB_CLOSING_POLICY".equals(p)) {
+            tab.getStyleClass().addListener(weakStyleClassListener);
+
+            listener.registerChangeListener(getSkinnable().tabClosingPolicyProperty(),e -> {
                 inner.requestLayout();
                 requestLayout(); 
-            } else if ("SIDE".equals(p)) {
+            });
+            listener.registerChangeListener(getSkinnable().sideProperty(),e -> {
                 final Side side = getSkinnable().getSide();
                 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
                 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
                 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
                 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
                 inner.setRotate(side == Side.BOTTOM ? 180.0F : 0.0F);
                 if (getSkinnable().isRotateGraphic()) {
                     updateGraphicRotation();
                 }
-            } else if ("ROTATE_GRAPHIC".equals(p)) {
-                updateGraphicRotation();
-            } else if ("TAB_MIN_WIDTH".equals(p)) {
+            });
+            listener.registerChangeListener(getSkinnable().rotateGraphicProperty(), e -> updateGraphicRotation());
+            listener.registerChangeListener(getSkinnable().tabMinWidthProperty(), e -> {
                 requestLayout();
                 getSkinnable().requestLayout();
-            } else if ("TAB_MAX_WIDTH".equals(p)) {
+            });
+            listener.registerChangeListener(getSkinnable().tabMaxWidthProperty(), e -> {
                 requestLayout();
                 getSkinnable().requestLayout();
-            } else if ("TAB_MIN_HEIGHT".equals(p)) {
+            });
+            listener.registerChangeListener(getSkinnable().tabMinHeightProperty(), e -> {
                 requestLayout();
                 getSkinnable().requestLayout();
-            } else if ("TAB_MAX_HEIGHT".equals(p)) {
+            });
+            listener.registerChangeListener(getSkinnable().tabMaxHeightProperty(), e -> {
                 requestLayout();
                 getSkinnable().requestLayout();
+            });
+
+            getProperties().put(Tab.class, tab);
+            getProperties().put(ContextMenu.class, tab.getContextMenu());
+
+            setOnContextMenuRequested((ContextMenuEvent me) -> {
+               if (getTab().getContextMenu() != null) {
+                    getTab().getContextMenu().show(inner, me.getScreenX(), me.getScreenY());
+                    me.consume();
+                }
+            });
+            setOnMousePressed(new EventHandler<MouseEvent>() {
+                @Override public void handle(MouseEvent me) {
+                    if (getTab().isDisable()) {
+                        return;
+                    }
+                    if (me.getButton().equals(MouseButton.MIDDLE)) {
+                        if (showCloseButton()) {
+                            Tab tab = getTab();
+                            if (behavior.canCloseTab(tab)) {
+                                removeListeners(tab);
+                                behavior.closeTab(tab);    
+                            }
+                        }
+                    } else if (me.getButton().equals(MouseButton.PRIMARY)) {
+                        behavior.selectTab(getTab());
+                    }
             } 
+            });    
+
+            // initialize pseudo-class state
+            pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
+            pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisable());
+            final Side side = getSkinnable().getSide();
+            pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
+            pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
+            pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
+            pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
         }
 
         private void updateGraphicRotation() {
             if (label.getGraphic() != null) {
                 label.getGraphic().setRotate(getSkinnable().isRotateGraphic() ? 0.0F :