1 /* 2 * Copyright (c) 2011, 2017, 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 com.sun.javafx.scene.control.TabObservableList; 35 import javafx.beans.property.BooleanProperty; 36 import javafx.beans.property.DoubleProperty; 37 import javafx.beans.property.ObjectProperty; 38 import javafx.beans.property.ObjectPropertyBase; 39 import javafx.beans.property.SimpleBooleanProperty; 40 import javafx.beans.property.SimpleObjectProperty; 41 import javafx.beans.value.WritableValue; 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 = new TabObservableList<>(new ArrayList<>()); 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 * @return the list of tabs 166 */ 167 public final ObservableList<Tab> getTabs() { 168 return tabs; 169 } 170 171 private ObjectProperty<SingleSelectionModel<Tab>> selectionModel = new SimpleObjectProperty<SingleSelectionModel<Tab>>(this, "selectionModel"); 172 173 /** 174 * <p>Sets the model used for tab selection. By changing the model you can alter 175 * how the tabs are selected and which tabs are first or last.</p> 176 * @param value the selection model 177 */ 178 public final void setSelectionModel(SingleSelectionModel<Tab> value) { selectionModel.set(value); } 179 180 /** 181 * <p>Gets the model used for tab selection.</p> 182 * @return the model used for tab selection 183 */ 184 public final SingleSelectionModel<Tab> getSelectionModel() { return selectionModel.get(); } 185 186 /** 187 * The selection model used for selecting tabs. 188 * @return selection model property 189 */ 190 public final ObjectProperty<SingleSelectionModel<Tab>> selectionModelProperty() { return selectionModel; } 191 192 private ObjectProperty<Side> side; 193 194 /** 195 * <p>The position to place the tabs in this TabPane. Whenever this changes 196 * the TabPane will immediately update the location of the tabs to reflect 197 * this.</p> 198 * 199 * @param value the side 200 */ 201 public final void setSide(Side value) { 202 sideProperty().set(value); 203 } 204 205 /** 206 * The current position of the tabs in the TabPane. The default position 207 * for the tabs is Side.Top. 208 * 209 * @return The current position of the tabs in the TabPane. 210 */ 211 public final Side getSide() { 212 return side == null ? Side.TOP : side.get(); 213 } 214 215 /** 216 * The position of the tabs in the TabPane. 217 * @return the side property 218 */ 219 public final ObjectProperty<Side> sideProperty() { 220 if (side == null) { 221 side = new ObjectPropertyBase<Side>(Side.TOP) { 222 private Side oldSide; 223 @Override protected void invalidated() { 224 225 oldSide = get(); 226 227 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (oldSide == Side.TOP || oldSide == null)); 228 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (oldSide == Side.RIGHT)); 229 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (oldSide == Side.BOTTOM)); 230 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (oldSide == Side.LEFT)); 231 } 232 233 @Override 234 public Object getBean() { 235 return TabPane.this; 236 } 237 238 @Override 239 public String getName() { 240 return "side"; 241 } 242 }; 243 } 244 return side; 245 } 246 247 private ObjectProperty<TabClosingPolicy> tabClosingPolicy; 248 249 /** 250 * <p>Specifies how the TabPane handles tab closing from an end-users 251 * perspective. The options are:</p> 252 * 253 * <ul> 254 * <li> TabClosingPolicy.UNAVAILABLE: Tabs can not be closed by the user 255 * <li> TabClosingPolicy.SELECTED_TAB: Only the currently selected tab will 256 * have the option to be closed, with a graphic next to the tab 257 * text being shown. The graphic will disappear when a tab is no 258 * longer selected. 259 * <li> TabClosingPolicy.ALL_TABS: All tabs will have the option to be 260 * closed. 261 * </ul> 262 * 263 * <p>Refer to the {@link TabClosingPolicy} enumeration for further details.</p> 264 * 265 * The default closing policy is TabClosingPolicy.SELECTED_TAB 266 * @param value the closing policy 267 */ 268 public final void setTabClosingPolicy(TabClosingPolicy value) { 269 tabClosingPolicyProperty().set(value); 270 } 271 272 /** 273 * The closing policy for the tabs. 274 * 275 * @return The closing policy for the tabs. 276 */ 277 public final TabClosingPolicy getTabClosingPolicy() { 278 return tabClosingPolicy == null ? TabClosingPolicy.SELECTED_TAB : tabClosingPolicy.get(); 279 } 280 281 /** 282 * The closing policy for the tabs. 283 * @return the closing policy property 284 */ 285 public final ObjectProperty<TabClosingPolicy> tabClosingPolicyProperty() { 286 if (tabClosingPolicy == null) { 287 tabClosingPolicy = new SimpleObjectProperty<TabClosingPolicy>(this, "tabClosingPolicy", TabClosingPolicy.SELECTED_TAB); 288 } 289 return tabClosingPolicy; 290 } 291 292 private BooleanProperty rotateGraphic; 293 294 /** 295 * <p>Specifies whether the graphic inside a Tab is rotated or not, such 296 * that it is always upright, or rotated in the same way as the Tab text is.</p> 297 * 298 * <p>By default rotateGraphic is set to false, to represent the fact that 299 * the graphic isn't rotated, resulting in it always appearing upright. If 300 * rotateGraphic is set to {@code true}, the graphic will rotate such that it 301 * rotates with the tab text.</p> 302 * 303 * @param value a flag indicating whether to rotate the graphic 304 */ 305 public final void setRotateGraphic(boolean value) { 306 rotateGraphicProperty().set(value); 307 } 308 309 /** 310 * Returns {@code true} if the graphic inside a Tab is rotated. The 311 * default is {@code false} 312 * 313 * @return the rotatedGraphic state. 314 */ 315 public final boolean isRotateGraphic() { 316 return rotateGraphic == null ? false : rotateGraphic.get(); 317 } 318 319 /** 320 * The rotateGraphic state of the tabs in the TabPane. 321 * @return the rotateGraphic property 322 */ 323 public final BooleanProperty rotateGraphicProperty() { 324 if (rotateGraphic == null) { 325 rotateGraphic = new SimpleBooleanProperty(this, "rotateGraphic", false); 326 } 327 return rotateGraphic; 328 } 329 330 private DoubleProperty tabMinWidth; 331 332 /** 333 * <p>The minimum width of the tabs in the TabPane. This can be used to limit 334 * the length of text in tabs to prevent truncation. Setting the min equal 335 * to the max will fix the width of the tab. By default the min equals to the max. 336 * 337 * This value can also be set via CSS using {@code -fx-tab-min-width} 338 * 339 * </p> 340 * @param value the minimum width of the tabs 341 */ 342 public final void setTabMinWidth(double value) { 343 tabMinWidthProperty().setValue(value); 344 } 345 346 /** 347 * The minimum width of the tabs in the TabPane. 348 * 349 * @return The minimum width of the tabs 350 */ 351 public final double getTabMinWidth() { 352 return tabMinWidth == null ? DEFAULT_TAB_MIN_WIDTH : tabMinWidth.getValue(); 353 } 354 355 /** 356 * The minimum width of the tabs in the TabPane. 357 * @return the minimum width property 358 */ 359 public final DoubleProperty tabMinWidthProperty() { 360 if (tabMinWidth == null) { 361 tabMinWidth = new StyleableDoubleProperty(DEFAULT_TAB_MIN_WIDTH) { 362 363 @Override 364 public CssMetaData<TabPane,Number> getCssMetaData() { 365 return StyleableProperties.TAB_MIN_WIDTH; 366 } 367 368 @Override 369 public Object getBean() { 370 return TabPane.this; 371 } 372 373 @Override 374 public String getName() { 375 return "tabMinWidth"; 376 } 377 }; 378 } 379 return tabMinWidth; 380 } 381 382 /** 383 * <p>Specifies the maximum width of a tab. This can be used to limit 384 * the length of text in tabs. If the tab text is longer than the maximum 385 * width the text will be truncated. Setting the max equal 386 * to the min will fix the width of the tab. By default the min equals to the max 387 * 388 * This value can also be set via CSS using {@code -fx-tab-max-width}.</p> 389 */ 390 private DoubleProperty tabMaxWidth; 391 public final void setTabMaxWidth(double value) { 392 tabMaxWidthProperty().setValue(value); 393 } 394 395 /** 396 * The maximum width of the tabs in the TabPane. 397 * 398 * @return The maximum width of the tabs 399 */ 400 public final double getTabMaxWidth() { 401 return tabMaxWidth == null ? DEFAULT_TAB_MAX_WIDTH : tabMaxWidth.getValue(); 402 } 403 404 /** 405 * The maximum width of the tabs in the TabPane. 406 * @return the maximum width property 407 */ 408 public final DoubleProperty tabMaxWidthProperty() { 409 if (tabMaxWidth == null) { 410 tabMaxWidth = new StyleableDoubleProperty(DEFAULT_TAB_MAX_WIDTH) { 411 412 @Override 413 public CssMetaData<TabPane,Number> getCssMetaData() { 414 return StyleableProperties.TAB_MAX_WIDTH; 415 } 416 417 @Override 418 public Object getBean() { 419 return TabPane.this; 420 } 421 422 @Override 423 public String getName() { 424 return "tabMaxWidth"; 425 } 426 }; 427 } 428 return tabMaxWidth; 429 } 430 431 private DoubleProperty tabMinHeight; 432 433 /** 434 * <p>The minimum height of the tabs in the TabPane. This can be used to limit 435 * the height in tabs. Setting the min equal to the max will fix the height 436 * of the tab. By default the min equals to the max. 437 * 438 * This value can also be set via CSS using {@code -fx-tab-min-height} 439 * </p> 440 * @param value the minimum height of the tabs 441 */ 442 public final void setTabMinHeight(double value) { 443 tabMinHeightProperty().setValue(value); 444 } 445 446 /** 447 * The minimum height of the tabs in the TabPane. 448 * 449 * @return the minimum height of the tabs 450 */ 451 public final double getTabMinHeight() { 452 return tabMinHeight == null ? DEFAULT_TAB_MIN_HEIGHT : tabMinHeight.getValue(); 453 } 454 455 /** 456 * The minimum height of the tab. 457 * @return the minimum height property 458 */ 459 public final DoubleProperty tabMinHeightProperty() { 460 if (tabMinHeight == null) { 461 tabMinHeight = new StyleableDoubleProperty(DEFAULT_TAB_MIN_HEIGHT) { 462 463 @Override 464 public CssMetaData<TabPane,Number> getCssMetaData() { 465 return StyleableProperties.TAB_MIN_HEIGHT; 466 } 467 468 @Override 469 public Object getBean() { 470 return TabPane.this; 471 } 472 473 @Override 474 public String getName() { 475 return "tabMinHeight"; 476 } 477 }; 478 } 479 return tabMinHeight; 480 } 481 482 /** 483 * <p>The maximum height if the tabs in the TabPane. This can be used to limit 484 * the height in tabs. Setting the max equal to the min will fix the height 485 * of the tab. By default the min equals to the max. 486 * 487 * This value can also be set via CSS using -fx-tab-max-height 488 * </p> 489 */ 490 private DoubleProperty tabMaxHeight; 491 public final void setTabMaxHeight(double value) { 492 tabMaxHeightProperty().setValue(value); 493 } 494 495 /** 496 * The maximum height of the tabs in the TabPane. 497 * 498 * @return The maximum height of the tabs 499 */ 500 public final double getTabMaxHeight() { 501 return tabMaxHeight == null ? DEFAULT_TAB_MAX_HEIGHT : tabMaxHeight.getValue(); 502 } 503 504 /** 505 * <p>The maximum height of the tabs in the TabPane.</p> 506 * @return the maximum height of the tabs 507 */ 508 public final DoubleProperty tabMaxHeightProperty() { 509 if (tabMaxHeight == null) { 510 tabMaxHeight = new StyleableDoubleProperty(DEFAULT_TAB_MAX_HEIGHT) { 511 512 @Override 513 public CssMetaData<TabPane,Number> getCssMetaData() { 514 return StyleableProperties.TAB_MAX_HEIGHT; 515 } 516 517 @Override 518 public Object getBean() { 519 return TabPane.this; 520 } 521 522 @Override 523 public String getName() { 524 return "tabMaxHeight"; 525 } 526 }; 527 } 528 return tabMaxHeight; 529 } 530 531 /** {@inheritDoc} */ 532 @Override protected Skin<?> createDefaultSkin() { 533 return new TabPaneSkin(this); 534 } 535 536 /** {@inheritDoc} */ 537 @Override public Node lookup(String selector) { 538 Node n = super.lookup(selector); 539 if (n == null) { 540 for(Tab tab : tabs) { 541 n = tab.lookup(selector); 542 if (n != null) break; 543 } 544 } 545 return n; 546 } 547 548 /** {@inheritDoc} */ 549 public Set<Node> lookupAll(String selector) { 550 551 if (selector == null) return null; 552 553 final List<Node> results = new ArrayList<>(); 554 555 results.addAll(super.lookupAll(selector)); 556 for(Tab tab : tabs) { 557 results.addAll(tab.lookupAll(selector)); 558 } 559 560 return new UnmodifiableListSet<Node>(results); 561 } 562 563 564 /*************************************************************************** 565 * * 566 * Stylesheet Handling * 567 * * 568 **************************************************************************/ 569 570 private static class StyleableProperties { 571 private static final CssMetaData<TabPane,Number> TAB_MIN_WIDTH = 572 new CssMetaData<TabPane,Number>("-fx-tab-min-width", 573 SizeConverter.getInstance(), DEFAULT_TAB_MIN_WIDTH) { 574 575 @Override 576 public boolean isSettable(TabPane n) { 577 return n.tabMinWidth == null || !n.tabMinWidth.isBound(); 578 } 579 580 @Override 581 public StyleableProperty<Number> getStyleableProperty(TabPane n) { 582 return (StyleableProperty<Number>)(WritableValue<Number>)n.tabMinWidthProperty(); 583 } 584 }; 585 586 private static final CssMetaData<TabPane,Number> TAB_MAX_WIDTH = 587 new CssMetaData<TabPane,Number>("-fx-tab-max-width", 588 SizeConverter.getInstance(), DEFAULT_TAB_MAX_WIDTH) { 589 590 @Override 591 public boolean isSettable(TabPane n) { 592 return n.tabMaxWidth == null || !n.tabMaxWidth.isBound(); 593 } 594 595 @Override 596 public StyleableProperty<Number> getStyleableProperty(TabPane n) { 597 return (StyleableProperty<Number>)(WritableValue<Number>)n.tabMaxWidthProperty(); 598 } 599 }; 600 601 private static final CssMetaData<TabPane,Number> TAB_MIN_HEIGHT = 602 new CssMetaData<TabPane,Number>("-fx-tab-min-height", 603 SizeConverter.getInstance(), DEFAULT_TAB_MIN_HEIGHT) { 604 605 @Override 606 public boolean isSettable(TabPane n) { 607 return n.tabMinHeight == null || !n.tabMinHeight.isBound(); 608 } 609 610 @Override 611 public StyleableProperty<Number> getStyleableProperty(TabPane n) { 612 return (StyleableProperty<Number>)(WritableValue<Number>)n.tabMinHeightProperty(); 613 } 614 }; 615 616 private static final CssMetaData<TabPane,Number> TAB_MAX_HEIGHT = 617 new CssMetaData<TabPane,Number>("-fx-tab-max-height", 618 SizeConverter.getInstance(), DEFAULT_TAB_MAX_HEIGHT) { 619 620 @Override 621 public boolean isSettable(TabPane n) { 622 return n.tabMaxHeight == null || !n.tabMaxHeight.isBound(); 623 } 624 625 @Override 626 public StyleableProperty<Number> getStyleableProperty(TabPane n) { 627 return (StyleableProperty<Number>)(WritableValue<Number>)n.tabMaxHeightProperty(); 628 } 629 }; 630 631 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 632 static { 633 final List<CssMetaData<? extends Styleable, ?>> styleables = 634 new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData()); 635 styleables.add(TAB_MIN_WIDTH); 636 styleables.add(TAB_MAX_WIDTH); 637 styleables.add(TAB_MIN_HEIGHT); 638 styleables.add(TAB_MAX_HEIGHT); 639 STYLEABLES = Collections.unmodifiableList(styleables); 640 } 641 } 642 643 /** 644 * @return The CssMetaData associated with this class, which may include the 645 * CssMetaData of its superclasses. 646 * @since JavaFX 8.0 647 */ 648 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 649 return StyleableProperties.STYLEABLES; 650 } 651 652 /** 653 * {@inheritDoc} 654 * @since JavaFX 8.0 655 */ 656 @Override 657 public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() { 658 return getClassCssMetaData(); 659 } 660 661 private static final PseudoClass TOP_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("top"); 662 private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("bottom"); 663 private static final PseudoClass LEFT_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("left"); 664 private static final PseudoClass RIGHT_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("right"); 665 666 /*************************************************************************** 667 * * 668 * Support classes * 669 * * 670 **************************************************************************/ 671 672 static class TabPaneSelectionModel extends SingleSelectionModel<Tab> { 673 private final TabPane tabPane; 674 675 public TabPaneSelectionModel(final TabPane t) { 676 if (t == null) { 677 throw new NullPointerException("TabPane can not be null"); 678 } 679 this.tabPane = t; 680 681 // watching for changes to the items list content 682 final ListChangeListener<Tab> itemsContentObserver = c -> { 683 while (c.next()) { 684 for (Tab tab : c.getRemoved()) { 685 if (tab != null && !tabPane.getTabs().contains(tab)) { 686 if (tab.isSelected()) { 687 tab.setSelected(false); 688 final int tabIndex = c.getFrom(); 689 690 // we always try to select the nearest, non-disabled 691 // tab from the position of the closed tab. 692 findNearestAvailableTab(tabIndex, true); 693 } 694 } 695 } 696 if (c.wasAdded() || c.wasRemoved()) { 697 // The selected tab index can be out of sync with the list of tab if 698 // we add or remove tabs before the selected tab. 699 if (getSelectedIndex() != tabPane.getTabs().indexOf(getSelectedItem())) { 700 clearAndSelect(tabPane.getTabs().indexOf(getSelectedItem())); 701 } 702 } 703 } 704 if (getSelectedIndex() == -1 && getSelectedItem() == null && tabPane.getTabs().size() > 0) { 705 // we go looking for the first non-disabled tab, as opposed to 706 // just selecting the first tab (fix for RT-36908) 707 findNearestAvailableTab(0, true); 708 } else if (tabPane.getTabs().isEmpty()) { 709 clearSelection(); 710 } 711 }; 712 if (this.tabPane.getTabs() != null) { 713 this.tabPane.getTabs().addListener(itemsContentObserver); 714 } 715 } 716 717 // API Implementation 718 @Override public void select(int index) { 719 if (index < 0 || (getItemCount() > 0 && index >= getItemCount()) || 720 (index == getSelectedIndex() && getModelItem(index).isSelected())) { 721 return; 722 } 723 724 // Unselect the old tab 725 if (getSelectedIndex() >= 0 && getSelectedIndex() < tabPane.getTabs().size()) { 726 tabPane.getTabs().get(getSelectedIndex()).setSelected(false); 727 } 728 729 setSelectedIndex(index); 730 731 Tab tab = getModelItem(index); 732 if (tab != null) { 733 setSelectedItem(tab); 734 } 735 736 // Select the new tab 737 if (getSelectedIndex() >= 0 && getSelectedIndex() < tabPane.getTabs().size()) { 738 tabPane.getTabs().get(getSelectedIndex()).setSelected(true); 739 } 740 741 /* Does this get all the change events */ 742 tabPane.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM); 743 } 744 745 @Override public void select(Tab tab) { 746 final int itemCount = getItemCount(); 747 748 for (int i = 0; i < itemCount; i++) { 749 final Tab value = getModelItem(i); 750 if (value != null && value.equals(tab)) { 751 select(i); 752 return; 753 } 754 } 755 if (tab != null) { 756 setSelectedItem(tab); 757 } 758 } 759 760 @Override protected Tab getModelItem(int index) { 761 final ObservableList<Tab> items = tabPane.getTabs(); 762 if (items == null) return null; 763 if (index < 0 || index >= items.size()) return null; 764 return items.get(index); 765 } 766 767 @Override protected int getItemCount() { 768 final ObservableList<Tab> items = tabPane.getTabs(); 769 return items == null ? 0 : items.size(); 770 } 771 772 private Tab findNearestAvailableTab(int tabIndex, boolean doSelect) { 773 // we always try to select the nearest, non-disabled 774 // tab from the position of the closed tab. 775 final int tabCount = getItemCount(); 776 int i = 1; 777 Tab bestTab = null; 778 while (true) { 779 // look leftwards 780 int downPos = tabIndex - i; 781 if (downPos >= 0) { 782 Tab _tab = getModelItem(downPos); 783 if (_tab != null && ! _tab.isDisable()) { 784 bestTab = _tab; 785 break; 786 } 787 } 788 789 // look rightwards. We subtract one as we need 790 // to take into account that a tab has been removed 791 // and if we don't do this we'll miss the tab 792 // to the right of the tab (as it has moved into 793 // the removed tabs position). 794 int upPos = tabIndex + i - 1; 795 if (upPos < tabCount) { 796 Tab _tab = getModelItem(upPos); 797 if (_tab != null && ! _tab.isDisable()) { 798 bestTab = _tab; 799 break; 800 } 801 } 802 803 if (downPos < 0 && upPos >= tabCount) { 804 break; 805 } 806 i++; 807 } 808 809 if (doSelect && bestTab != null) { 810 select(bestTab); 811 } 812 813 return bestTab; 814 } 815 } 816 817 /** 818 * <p>This specifies how the TabPane handles tab closing from an end-users 819 * perspective. The options are:</p> 820 * 821 * <ul> 822 * <li> TabClosingPolicy.UNAVAILABLE: Tabs can not be closed by the user 823 * <li> TabClosingPolicy.SELECTED_TAB: Only the currently selected tab will 824 * have the option to be closed, with a graphic next to the tab 825 * text being shown. The graphic will disappear when a tab is no 826 * longer selected. 827 * <li> TabClosingPolicy.ALL_TABS: All tabs will have the option to be 828 * closed. 829 * </ul> 830 * @since JavaFX 2.0 831 */ 832 public enum TabClosingPolicy { 833 834 /** 835 * Only the currently selected tab will have the option to be closed, with a 836 * graphic next to the tab text being shown. The graphic will disappear when 837 * a tab is no longer selected. 838 */ 839 SELECTED_TAB, 840 841 /** 842 * All tabs will have the option to be closed. 843 */ 844 ALL_TABS, 845 846 /** 847 * Tabs can not be closed by the user. 848 */ 849 UNAVAILABLE 850 } 851 852 853 // TabDragPolicy // 854 private ObjectProperty<TabDragPolicy> tabDragPolicy; 855 856 /** 857 * The drag policy for the tabs. The policy can be changed dynamically. 858 * 859 * @defaultValue TabDragPolicy.FIXED 860 * @return The tab drag policy property 861 * @since 10 862 */ 863 public final ObjectProperty<TabDragPolicy> tabDragPolicyProperty() { 864 if (tabDragPolicy == null) { 865 tabDragPolicy = new SimpleObjectProperty<TabDragPolicy>(this, "tabDragPolicy", TabDragPolicy.FIXED); 866 } 867 return tabDragPolicy; 868 } 869 public final void setTabDragPolicy(TabDragPolicy value) { 870 tabDragPolicyProperty().set(value); 871 } 872 public final TabDragPolicy getTabDragPolicy() { 873 return tabDragPolicyProperty().get(); 874 } 875 876 /** 877 * This enum specifies drag policies for tabs in a TabPane. 878 * 879 * @since 10 880 */ 881 public enum TabDragPolicy { 882 /** 883 * The tabs remain fixed in their positions and cannot be dragged. 884 */ 885 FIXED, 886 887 /** 888 * The tabs can be dragged to reorder them within the same TabPane. 889 * Users can perform the simple mouse press-drag-release gesture on a 890 * tab header to drag it to a new position. A tab can not be detached 891 * from its parent TabPane. 892 * <p>After a tab is reordered, the {@link #getTabs() tabs} list is 893 * permuted to reflect the updated order. 894 * A {@link javafx.collections.ListChangeListener.Change permutation 895 * change} event is fired to indicate which tabs were reordered. This 896 * reordering is done after the mouse button is released. While a tab 897 * is being dragged, the list of tabs is unchanged.</p> 898 */ 899 REORDER 900 } 901 }