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