1 /*
   2  * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control;
  27 
  28 import java.util.ArrayList;
  29 import java.util.Collections;
  30 import java.util.List;
  31 import java.util.Set;
  32 
  33 import com.sun.javafx.collections.UnmodifiableListSet;
  34 import javafx.beans.property.BooleanProperty;
  35 import javafx.beans.property.DoubleProperty;
  36 import javafx.beans.property.ObjectProperty;
  37 import javafx.beans.property.ObjectPropertyBase;
  38 import javafx.beans.property.SimpleBooleanProperty;
  39 import javafx.beans.property.SimpleObjectProperty;
  40 import javafx.beans.value.WritableValue;
  41 import javafx.collections.FXCollections;
  42 import javafx.collections.ListChangeListener;
  43 import javafx.collections.ObservableList;
  44 import javafx.geometry.Side;
  45 import javafx.scene.AccessibleAttribute;
  46 import javafx.scene.AccessibleRole;
  47 import javafx.css.StyleableDoubleProperty;
  48 import javafx.css.CssMetaData;
  49 import javafx.css.PseudoClass;
  50 
  51 import javafx.css.converter.SizeConverter;
  52 import javafx.scene.control.skin.TabPaneSkin;
  53 
  54 import javafx.beans.DefaultProperty;
  55 import javafx.css.Styleable;
  56 import javafx.css.StyleableProperty;
  57 import javafx.scene.Node;
  58 
  59 /**
  60  * <p>A control that allows switching between a group of {@link Tab Tabs}.  Only one tab
  61  * is visible at a time. Tabs are added to the TabPane by using the {@link #getTabs}.</p>
  62  *
  63  * <p>Tabs in a TabPane can be positioned at any of the four sides by specifying the
  64  * {@link Side}. </p>
  65  *
  66  * <p>A TabPane has two modes floating or recessed.  Applying the styleclass STYLE_CLASS_FLOATING
  67  * will change the TabPane mode to floating.</p>
  68  *
  69  * <p>The tabs width and height can be set to a specific size by
  70  * setting the min and max for height and width.  TabPane default width will be
  71  * determined by the largest content width in the TabPane.  This is the same for the height.
  72  * If a different size is desired the width and height of the TabPane can
  73  * be overridden by setting the min, pref and max size.</p>
  74  *
  75  * <p>When the number of tabs do not fit the TabPane a menu button will appear on the right.
  76  * The menu button is used to select the tabs that are currently not visible.
  77  * </p>
  78  *
  79  * <p>Example:</p>
  80  * <pre><code>
  81  * TabPane tabPane = new TabPane();
  82  * Tab tab = new Tab();
  83  * tab.setText("new tab");
  84  * tab.setContent(new Rectangle(200,200, Color.LIGHTSTEELBLUE));
  85  * tabPane.getTabs().add(tab);
  86  * </code></pre>
  87  *
  88  * @see Tab
  89  * @since JavaFX 2.0
  90  */
  91 @DefaultProperty("tabs")
  92 public class TabPane extends Control {
  93     private static final double DEFAULT_TAB_MIN_WIDTH = 0;
  94 
  95     private static final double DEFAULT_TAB_MAX_WIDTH = Double.MAX_VALUE;
  96 
  97     private static final double DEFAULT_TAB_MIN_HEIGHT = 0;
  98 
  99     private static final double DEFAULT_TAB_MAX_HEIGHT = Double.MAX_VALUE;
 100 
 101     /**
 102      * TabPane mode will be changed to floating allowing the TabPane
 103      * to be placed alongside other control.
 104      */
 105     public static final String STYLE_CLASS_FLOATING = "floating";
 106 
 107     /**
 108      * Constructs a new TabPane.
 109      */
 110     public TabPane() {
 111         this((Tab[])null);
 112     }
 113 
 114     /**
 115      * Constructs a new TabPane with the given tabs set to show.
 116      *
 117      * @param tabs The {@link Tab tabs} to display inside the TabPane.
 118      * @since JavaFX 8u40
 119      */
 120     public TabPane(Tab... tabs) {
 121         getStyleClass().setAll("tab-pane");
 122         setAccessibleRole(AccessibleRole.TAB_PANE);
 123         setSelectionModel(new TabPaneSelectionModel(this));
 124 
 125         this.tabs.addListener((ListChangeListener<Tab>) c -> {
 126             while (c.next()) {
 127                 for (Tab tab : c.getRemoved()) {
 128                     if (tab != null && !getTabs().contains(tab)) {
 129                         tab.setTabPane(null);
 130                     }
 131                 }
 132 
 133                 for (Tab tab : c.getAddedSubList()) {
 134                     if (tab != null) {
 135                         tab.setTabPane(TabPane.this);
 136                     }
 137                 }
 138             }
 139         });
 140 
 141         if (tabs != null) {
 142             getTabs().addAll(tabs);
 143         }
 144 
 145         // initialize pseudo-class state
 146         Side edge = getSide();
 147         pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (edge == Side.TOP));
 148         pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (edge == Side.RIGHT));
 149         pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (edge == Side.BOTTOM));
 150         pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (edge == Side.LEFT));
 151 
 152     }
 153 
 154     private ObservableList<Tab> tabs = FXCollections.observableArrayList();
 155 
 156     /**
 157      * <p>The tabs to display in this TabPane. Changing this ObservableList will
 158      * immediately result in the TabPane updating to display the new contents
 159      * of this ObservableList.</p>
 160      *
 161      * <p>If the tabs ObservableList changes, the selected tab will remain the previously
 162      * selected tab, if it remains within this ObservableList. If the previously
 163      * selected tab is no longer in the tabs ObservableList, the selected tab will
 164      * become the first tab in the ObservableList.</p>
 165      */
 166     public final ObservableList<Tab> getTabs() {
 167         return tabs;
 168     }
 169 
 170     private ObjectProperty<SingleSelectionModel<Tab>> selectionModel = new SimpleObjectProperty<SingleSelectionModel<Tab>>(this, "selectionModel");
 171 
 172     /**
 173      * <p>Sets the model used for tab selection.  By changing the model you can alter
 174      * how the tabs are selected and which tabs are first or last.</p>
 175      */
 176     public final void setSelectionModel(SingleSelectionModel<Tab> value) { selectionModel.set(value); }
 177 
 178     /**
 179      * <p>Gets the model used for tab selection.</p>
 180      */
 181     public final SingleSelectionModel<Tab> getSelectionModel() { return selectionModel.get(); }
 182 
 183     /**
 184      * The selection model used for selecting tabs.
 185      */
 186     public final ObjectProperty<SingleSelectionModel<Tab>> selectionModelProperty() { return selectionModel; }
 187 
 188     private ObjectProperty<Side> side;
 189 
 190     /**
 191      * <p>The position to place the tabs in this TabPane. Whenever this changes
 192      * the TabPane will immediately update the location of the tabs to reflect
 193      * this.</p>
 194      *
 195      */
 196     public final void setSide(Side value) {
 197         sideProperty().set(value);
 198     }
 199 
 200     /**
 201      * The current position of the tabs in the TabPane.  The default position
 202      * for the tabs is Side.Top.
 203      *
 204      * @return The current position of the tabs in the TabPane.
 205      */
 206     public final Side getSide() {
 207         return side == null ? Side.TOP : side.get();
 208     }
 209 
 210     /**
 211      * The position of the tabs in the TabPane.
 212      */
 213     public final ObjectProperty<Side> sideProperty() {
 214         if (side == null) {
 215             side = new ObjectPropertyBase<Side>(Side.TOP) {
 216                 private Side oldSide;
 217                 @Override protected void invalidated() {
 218 
 219                     oldSide = get();
 220 
 221                     pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (oldSide == Side.TOP || oldSide == null));
 222                     pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (oldSide == Side.RIGHT));
 223                     pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (oldSide == Side.BOTTOM));
 224                     pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (oldSide == Side.LEFT));
 225                 }
 226 
 227                 @Override
 228                 public Object getBean() {
 229                     return TabPane.this;
 230                 }
 231 
 232                 @Override
 233                 public String getName() {
 234                     return "side";
 235                 }
 236             };
 237         }
 238         return side;
 239     }
 240 
 241     private ObjectProperty<TabClosingPolicy> tabClosingPolicy;
 242 
 243     /**
 244      * <p>Specifies how the TabPane handles tab closing from an end-users
 245      * perspective. The options are:</p>
 246      *
 247      * <ul>
 248      *   <li> TabClosingPolicy.UNAVAILABLE: Tabs can not be closed by the user
 249      *   <li> TabClosingPolicy.SELECTED_TAB: Only the currently selected tab will
 250      *          have the option to be closed, with a graphic next to the tab
 251      *          text being shown. The graphic will disappear when a tab is no
 252      *          longer selected.
 253      *   <li> TabClosingPolicy.ALL_TABS: All tabs will have the option to be
 254      *          closed.
 255      * </ul>
 256      *
 257      * <p>Refer to the {@link TabClosingPolicy} enumeration for further details.</p>
 258      *
 259      * The default closing policy is TabClosingPolicy.SELECTED_TAB
 260      */
 261     public final void setTabClosingPolicy(TabClosingPolicy value) {
 262         tabClosingPolicyProperty().set(value);
 263     }
 264 
 265     /**
 266      * The closing policy for the tabs.
 267      *
 268      * @return The closing policy for the tabs.
 269      */
 270     public final TabClosingPolicy getTabClosingPolicy() {
 271         return tabClosingPolicy == null ? TabClosingPolicy.SELECTED_TAB : tabClosingPolicy.get();
 272     }
 273 
 274     /**
 275      * The closing policy for the tabs.
 276      */
 277     public final ObjectProperty<TabClosingPolicy> tabClosingPolicyProperty() {
 278         if (tabClosingPolicy == null) {
 279             tabClosingPolicy = new SimpleObjectProperty<TabClosingPolicy>(this, "tabClosingPolicy", TabClosingPolicy.SELECTED_TAB);
 280         }
 281         return tabClosingPolicy;
 282     }
 283 
 284     private BooleanProperty rotateGraphic;
 285 
 286     /**
 287      * <p>Specifies whether the graphic inside a Tab is rotated or not, such
 288      * that it is always upright, or rotated in the same way as the Tab text is.</p>
 289      *
 290      * <p>By default rotateGraphic is set to false, to represent the fact that
 291      * the graphic isn't rotated, resulting in it always appearing upright. If
 292      * rotateGraphic is set to {@code true}, the graphic will rotate such that it
 293      * rotates with the tab text.</p>
 294      *
 295      */
 296     public final void setRotateGraphic(boolean value) {
 297         rotateGraphicProperty().set(value);
 298     }
 299 
 300     /**
 301      * Returns {@code true} if the graphic inside a Tab is rotated. The
 302      * default is {@code false}
 303      *
 304      * @return the rotatedGraphic state.
 305      */
 306     public final boolean isRotateGraphic() {
 307         return rotateGraphic == null ? false : rotateGraphic.get();
 308     }
 309 
 310     /**
 311      * The rotatedGraphic state of the tabs in the TabPane.
 312      */
 313     public final BooleanProperty rotateGraphicProperty() {
 314         if (rotateGraphic == null) {
 315             rotateGraphic = new SimpleBooleanProperty(this, "rotateGraphic", false);
 316         }
 317         return rotateGraphic;
 318     }
 319 
 320     private DoubleProperty tabMinWidth;
 321 
 322     /**
 323      * <p>The minimum width of the tabs in the TabPane.  This can be used to limit
 324      * the length of text in tabs to prevent truncation.  Setting the min equal
 325      * to the max will fix the width of the tab.  By default the min equals to the max.
 326      *
 327      * This value can also be set via CSS using {@code -fx-tab-min-width}
 328      *
 329      * </p>
 330      */
 331     public final void setTabMinWidth(double value) {
 332         tabMinWidthProperty().setValue(value);
 333     }
 334 
 335     /**
 336      * The minimum width of the tabs in the TabPane.
 337      *
 338      * @return The minimum width of the tabs.
 339      */
 340     public final double getTabMinWidth() {
 341         return tabMinWidth == null ? DEFAULT_TAB_MIN_WIDTH : tabMinWidth.getValue();
 342     }
 343 
 344     /**
 345      * The minimum width of the tabs in the TabPane.
 346      */
 347     public final DoubleProperty tabMinWidthProperty() {
 348         if (tabMinWidth == null) {
 349             tabMinWidth = new StyleableDoubleProperty(DEFAULT_TAB_MIN_WIDTH) {
 350 
 351                 @Override
 352                 public CssMetaData<TabPane,Number> getCssMetaData() {
 353                     return StyleableProperties.TAB_MIN_WIDTH;
 354                 }
 355 
 356                 @Override
 357                 public Object getBean() {
 358                     return TabPane.this;
 359                 }
 360 
 361                 @Override
 362                 public String getName() {
 363                     return "tabMinWidth";
 364                 }
 365             };
 366         }
 367         return tabMinWidth;
 368     }
 369 
 370     /**
 371      * <p>Specifies the maximum width of a tab.  This can be used to limit
 372      * the length of text in tabs.  If the tab text is longer than the maximum
 373      * width the text will be truncated.  Setting the max equal
 374      * to the min will fix the width of the tab.  By default the min equals to the max
 375      *
 376      * This value can also be set via CSS using {@code -fx-tab-max-width}.</p>
 377      */
 378     private DoubleProperty tabMaxWidth;
 379     public final void setTabMaxWidth(double value) {
 380         tabMaxWidthProperty().setValue(value);
 381     }
 382 
 383     /**
 384      * The maximum width of the tabs in the TabPane.
 385      *
 386      * @return The maximum width of the tabs.
 387      */
 388     public final double getTabMaxWidth() {
 389         return tabMaxWidth == null ? DEFAULT_TAB_MAX_WIDTH : tabMaxWidth.getValue();
 390     }
 391 
 392     /**
 393      * The maximum width of the tabs in the TabPane.
 394      */
 395     public final DoubleProperty tabMaxWidthProperty() {
 396         if (tabMaxWidth == null) {
 397             tabMaxWidth = new StyleableDoubleProperty(DEFAULT_TAB_MAX_WIDTH) {
 398 
 399                 @Override
 400                 public CssMetaData<TabPane,Number> getCssMetaData() {
 401                     return StyleableProperties.TAB_MAX_WIDTH;
 402                 }
 403 
 404                 @Override
 405                 public Object getBean() {
 406                     return TabPane.this;
 407                 }
 408 
 409                 @Override
 410                 public String getName() {
 411                     return "tabMaxWidth";
 412                 }
 413             };
 414         }
 415         return tabMaxWidth;
 416     }
 417 
 418     private DoubleProperty tabMinHeight;
 419 
 420     /**
 421      * <p>The minimum height of the tabs in the TabPane.  This can be used to limit
 422      * the height in tabs. Setting the min equal to the max will fix the height
 423      * of the tab.  By default the min equals to the max.
 424      *
 425      * This value can also be set via CSS using {@code -fx-tab-min-height}
 426      * </p>
 427      */
 428     public final void setTabMinHeight(double value) {
 429         tabMinHeightProperty().setValue(value);
 430     }
 431 
 432     /**
 433      * The minimum height of the tabs in the TabPane.
 434      *
 435      * @return The minimum height of the tabs.
 436      */
 437     public final double getTabMinHeight() {
 438         return tabMinHeight == null ? DEFAULT_TAB_MIN_HEIGHT : tabMinHeight.getValue();
 439     }
 440 
 441     /**
 442      * The minimum height of the tab.
 443      */
 444     public final DoubleProperty tabMinHeightProperty() {
 445         if (tabMinHeight == null) {
 446             tabMinHeight = new StyleableDoubleProperty(DEFAULT_TAB_MIN_HEIGHT) {
 447 
 448                 @Override
 449                 public CssMetaData<TabPane,Number> getCssMetaData() {
 450                     return StyleableProperties.TAB_MIN_HEIGHT;
 451                 }
 452 
 453                 @Override
 454                 public Object getBean() {
 455                     return TabPane.this;
 456                 }
 457 
 458                 @Override
 459                 public String getName() {
 460                     return "tabMinHeight";
 461                 }
 462             };
 463         }
 464         return tabMinHeight;
 465     }
 466 
 467     /**
 468      * <p>The maximum height if the tabs in the TabPane.  This can be used to limit
 469      * the height in tabs. Setting the max equal to the min will fix the height
 470      * of the tab.  By default the min equals to the max.
 471      *
 472      * This value can also be set via CSS using -fx-tab-max-height
 473      * </p>
 474      */
 475     private DoubleProperty tabMaxHeight;
 476     public final void setTabMaxHeight(double value) {
 477         tabMaxHeightProperty().setValue(value);
 478     }
 479 
 480     /**
 481      * The maximum height of the tabs in the TabPane.
 482      *
 483      * @return The maximum height of the tabs.
 484      */
 485     public final double getTabMaxHeight() {
 486         return tabMaxHeight == null ? DEFAULT_TAB_MAX_HEIGHT : tabMaxHeight.getValue();
 487     }
 488 
 489     /**
 490      * <p>The maximum height of the tabs in the TabPane.</p>
 491      */
 492     public final DoubleProperty tabMaxHeightProperty() {
 493         if (tabMaxHeight == null) {
 494             tabMaxHeight = new StyleableDoubleProperty(DEFAULT_TAB_MAX_HEIGHT) {
 495 
 496                 @Override
 497                 public CssMetaData<TabPane,Number> getCssMetaData() {
 498                     return StyleableProperties.TAB_MAX_HEIGHT;
 499                 }
 500 
 501                 @Override
 502                 public Object getBean() {
 503                     return TabPane.this;
 504                 }
 505 
 506                 @Override
 507                 public String getName() {
 508                     return "tabMaxHeight";
 509                 }
 510             };
 511         }
 512         return tabMaxHeight;
 513     }
 514 
 515     /** {@inheritDoc} */
 516     @Override protected Skin<?> createDefaultSkin() {
 517         return new TabPaneSkin(this);
 518     }
 519 
 520     /** {@inheritDoc} */
 521     @Override public Node lookup(String selector) {
 522         Node n = super.lookup(selector);
 523         if (n == null) {
 524             for(Tab tab : tabs) {
 525                 n = tab.lookup(selector);
 526                 if (n != null) break;
 527             }
 528         }
 529         return n;
 530     }
 531 
 532     /** {@inheritDoc} */
 533     public Set<Node> lookupAll(String selector) {
 534 
 535         if (selector == null) return null;
 536 
 537         final List<Node> results = new ArrayList<>();
 538 
 539         results.addAll(super.lookupAll(selector));
 540         for(Tab tab : tabs) {
 541             results.addAll(tab.lookupAll(selector));
 542         }
 543 
 544         return new UnmodifiableListSet<Node>(results);
 545     }
 546 
 547 
 548     /***************************************************************************
 549      *                                                                         *
 550      *                         Stylesheet Handling                             *
 551      *                                                                         *
 552      **************************************************************************/
 553 
 554     private static class StyleableProperties {
 555         private static final CssMetaData<TabPane,Number> TAB_MIN_WIDTH =
 556                 new CssMetaData<TabPane,Number>("-fx-tab-min-width",
 557                 SizeConverter.getInstance(), DEFAULT_TAB_MIN_WIDTH) {
 558 
 559             @Override
 560             public boolean isSettable(TabPane n) {
 561                 return n.tabMinWidth == null || !n.tabMinWidth.isBound();
 562             }
 563 
 564             @Override
 565             public StyleableProperty<Number> getStyleableProperty(TabPane n) {
 566                 return (StyleableProperty<Number>)(WritableValue<Number>)n.tabMinWidthProperty();
 567             }
 568         };
 569 
 570         private static final CssMetaData<TabPane,Number> TAB_MAX_WIDTH =
 571                 new CssMetaData<TabPane,Number>("-fx-tab-max-width",
 572                 SizeConverter.getInstance(), DEFAULT_TAB_MAX_WIDTH) {
 573 
 574             @Override
 575             public boolean isSettable(TabPane n) {
 576                 return n.tabMaxWidth == null || !n.tabMaxWidth.isBound();
 577             }
 578 
 579             @Override
 580             public StyleableProperty<Number> getStyleableProperty(TabPane n) {
 581                 return (StyleableProperty<Number>)(WritableValue<Number>)n.tabMaxWidthProperty();
 582             }
 583         };
 584 
 585         private static final CssMetaData<TabPane,Number> TAB_MIN_HEIGHT =
 586                 new CssMetaData<TabPane,Number>("-fx-tab-min-height",
 587                 SizeConverter.getInstance(), DEFAULT_TAB_MIN_HEIGHT) {
 588 
 589             @Override
 590             public boolean isSettable(TabPane n) {
 591                 return n.tabMinHeight == null || !n.tabMinHeight.isBound();
 592             }
 593 
 594             @Override
 595             public StyleableProperty<Number> getStyleableProperty(TabPane n) {
 596                 return (StyleableProperty<Number>)(WritableValue<Number>)n.tabMinHeightProperty();
 597             }
 598         };
 599 
 600         private static final CssMetaData<TabPane,Number> TAB_MAX_HEIGHT =
 601                 new CssMetaData<TabPane,Number>("-fx-tab-max-height",
 602                 SizeConverter.getInstance(), DEFAULT_TAB_MAX_HEIGHT) {
 603 
 604             @Override
 605             public boolean isSettable(TabPane n) {
 606                 return n.tabMaxHeight == null || !n.tabMaxHeight.isBound();
 607             }
 608 
 609             @Override
 610             public StyleableProperty<Number> getStyleableProperty(TabPane n) {
 611                 return (StyleableProperty<Number>)(WritableValue<Number>)n.tabMaxHeightProperty();
 612             }
 613         };
 614 
 615         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 616         static {
 617             final List<CssMetaData<? extends Styleable, ?>> styleables =
 618                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData());
 619             styleables.add(TAB_MIN_WIDTH);
 620             styleables.add(TAB_MAX_WIDTH);
 621             styleables.add(TAB_MIN_HEIGHT);
 622             styleables.add(TAB_MAX_HEIGHT);
 623             STYLEABLES = Collections.unmodifiableList(styleables);
 624         }
 625     }
 626 
 627     /**
 628      * @return The CssMetaData associated with this class, which may include the
 629      * CssMetaData of its superclasses.
 630      * @since JavaFX 8.0
 631      */
 632     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 633         return StyleableProperties.STYLEABLES;
 634     }
 635 
 636     /**
 637      * {@inheritDoc}
 638      * @since JavaFX 8.0
 639      */
 640     @Override
 641     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
 642         return getClassCssMetaData();
 643     }
 644 
 645     private static final PseudoClass TOP_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("top");
 646     private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("bottom");
 647     private static final PseudoClass LEFT_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("left");
 648     private static final PseudoClass RIGHT_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("right");
 649 
 650     /***************************************************************************
 651      *                                                                         *
 652      * Support classes                                                         *
 653      *                                                                         *
 654      **************************************************************************/
 655 
 656     static class TabPaneSelectionModel extends SingleSelectionModel<Tab> {
 657         private final TabPane tabPane;
 658 
 659         public TabPaneSelectionModel(final TabPane t) {
 660             if (t == null) {
 661                 throw new NullPointerException("TabPane can not be null");
 662             }
 663             this.tabPane = t;
 664 
 665             // watching for changes to the items list content
 666             final ListChangeListener<Tab> itemsContentObserver = c -> {
 667                 while (c.next()) {
 668                     for (Tab tab : c.getRemoved()) {
 669                         if (tab != null && !tabPane.getTabs().contains(tab)) {
 670                             if (tab.isSelected()) {
 671                                 tab.setSelected(false);
 672                                 final int tabIndex = c.getFrom();
 673 
 674                                 // we always try to select the nearest, non-disabled
 675                                 // tab from the position of the closed tab.
 676                                 findNearestAvailableTab(tabIndex, true);
 677                             }
 678                         }
 679                     }
 680                     if (c.wasAdded() || c.wasRemoved()) {
 681                         // The selected tab index can be out of sync with the list of tab if
 682                         // we add or remove tabs before the selected tab.
 683                         if (getSelectedIndex() != tabPane.getTabs().indexOf(getSelectedItem())) {
 684                             clearAndSelect(tabPane.getTabs().indexOf(getSelectedItem()));
 685                         }
 686                     }
 687                 }
 688                 if (getSelectedIndex() == -1 && getSelectedItem() == null && tabPane.getTabs().size() > 0) {
 689                     // we go looking for the first non-disabled tab, as opposed to
 690                     // just selecting the first tab (fix for RT-36908)
 691                     findNearestAvailableTab(0, true);
 692                 } else if (tabPane.getTabs().isEmpty()) {
 693                     clearSelection();
 694                 }
 695             };
 696             if (this.tabPane.getTabs() != null) {
 697                 this.tabPane.getTabs().addListener(itemsContentObserver);
 698             }
 699         }
 700 
 701         // API Implementation
 702         @Override public void select(int index) {
 703             if (index < 0 || (getItemCount() > 0 && index >= getItemCount()) ||
 704                 (index == getSelectedIndex() && getModelItem(index).isSelected())) {
 705                 return;
 706             }
 707 
 708             // Unselect the old tab
 709             if (getSelectedIndex() >= 0 && getSelectedIndex() < tabPane.getTabs().size()) {
 710                 tabPane.getTabs().get(getSelectedIndex()).setSelected(false);
 711             }
 712 
 713             setSelectedIndex(index);
 714 
 715             Tab tab = getModelItem(index);
 716             if (tab != null) {
 717                 setSelectedItem(tab);
 718             }
 719 
 720             // Select the new tab
 721             if (getSelectedIndex() >= 0 && getSelectedIndex() < tabPane.getTabs().size()) {
 722                 tabPane.getTabs().get(getSelectedIndex()).setSelected(true);
 723             }
 724 
 725             /* Does this get all the change events */
 726             tabPane.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM);
 727         }
 728 
 729         @Override public void select(Tab tab) {
 730             final int itemCount = getItemCount();
 731 
 732             for (int i = 0; i < itemCount; i++) {
 733                 final Tab value = getModelItem(i);
 734                 if (value != null && value.equals(tab)) {
 735                     select(i);
 736                     return;
 737                 }
 738             }
 739             if (tab != null) {
 740                 setSelectedItem(tab);
 741             }
 742         }
 743 
 744         @Override protected Tab getModelItem(int index) {
 745             final ObservableList<Tab> items = tabPane.getTabs();
 746             if (items == null) return null;
 747             if (index < 0 || index >= items.size()) return null;
 748             return items.get(index);
 749         }
 750 
 751         @Override protected int getItemCount() {
 752             final ObservableList<Tab> items = tabPane.getTabs();
 753             return items == null ? 0 : items.size();
 754         }
 755 
 756         private Tab findNearestAvailableTab(int tabIndex, boolean doSelect) {
 757             // we always try to select the nearest, non-disabled
 758             // tab from the position of the closed tab.
 759             final int tabCount = getItemCount();
 760             int i = 1;
 761             Tab bestTab = null;
 762             while (true) {
 763                 // look leftwards
 764                 int downPos = tabIndex - i;
 765                 if (downPos >= 0) {
 766                     Tab _tab = getModelItem(downPos);
 767                     if (_tab != null && ! _tab.isDisable()) {
 768                         bestTab = _tab;
 769                         break;
 770                     }
 771                 }
 772 
 773                 // look rightwards. We subtract one as we need
 774                 // to take into account that a tab has been removed
 775                 // and if we don't do this we'll miss the tab
 776                 // to the right of the tab (as it has moved into
 777                 // the removed tabs position).
 778                 int upPos = tabIndex + i - 1;
 779                 if (upPos < tabCount) {
 780                     Tab _tab = getModelItem(upPos);
 781                     if (_tab != null && ! _tab.isDisable()) {
 782                         bestTab = _tab;
 783                         break;
 784                     }
 785                 }
 786 
 787                 if (downPos < 0 && upPos >= tabCount) {
 788                     break;
 789                 }
 790                 i++;
 791             }
 792 
 793             if (doSelect && bestTab != null) {
 794                 select(bestTab);
 795             }
 796 
 797             return bestTab;
 798         }
 799     }
 800 
 801     /**
 802      * <p>This specifies how the TabPane handles tab closing from an end-users
 803      * perspective. The options are:</p>
 804      *
 805      * <ul>
 806      *   <li> TabClosingPolicy.UNAVAILABLE: Tabs can not be closed by the user
 807      *   <li> TabClosingPolicy.SELECTED_TAB: Only the currently selected tab will
 808      *          have the option to be closed, with a graphic next to the tab
 809      *          text being shown. The graphic will disappear when a tab is no
 810      *          longer selected.
 811      *   <li> TabClosingPolicy.ALL_TABS: All tabs will have the option to be
 812      *          closed.
 813      * </ul>
 814      * @since JavaFX 2.0
 815      */
 816     public enum TabClosingPolicy {
 817 
 818         /**
 819          * Only the currently selected tab will have the option to be closed, with a
 820          * graphic next to the tab text being shown. The graphic will disappear when
 821          * a tab is no longer selected.
 822          */
 823         SELECTED_TAB,
 824 
 825         /**
 826          * All tabs will have the option to be closed.
 827          */
 828         ALL_TABS,
 829 
 830         /**
 831          * Tabs can not be closed by the user.
 832          */
 833         UNAVAILABLE
 834     }
 835 }