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.skin; 27 28 import com.sun.javafx.scene.control.LambdaMultiplePropertyChangeListenerHandler; 29 import com.sun.javafx.scene.control.Properties; 30 import com.sun.javafx.scene.control.TabObservableList; 31 import com.sun.javafx.util.Utils; 32 import javafx.animation.Animation; 33 import javafx.animation.Interpolator; 34 import javafx.animation.KeyFrame; 35 import javafx.animation.KeyValue; 36 import javafx.animation.Timeline; 37 import javafx.animation.Transition; 38 import javafx.beans.InvalidationListener; 39 import javafx.beans.Observable; 40 import javafx.beans.WeakInvalidationListener; 41 import javafx.beans.property.DoubleProperty; 42 import javafx.beans.property.ObjectProperty; 43 import javafx.beans.property.SimpleDoubleProperty; 44 import javafx.beans.value.WritableValue; 45 import javafx.collections.FXCollections; 46 import javafx.collections.ListChangeListener; 47 import javafx.collections.ObservableList; 48 import javafx.collections.WeakListChangeListener; 49 import javafx.css.CssMetaData; 50 import javafx.css.PseudoClass; 51 import javafx.css.Styleable; 52 import javafx.css.StyleableObjectProperty; 53 import javafx.css.StyleableProperty; 54 import javafx.event.ActionEvent; 55 import javafx.event.EventHandler; 56 import javafx.geometry.Bounds; 57 import javafx.geometry.HPos; 58 import javafx.geometry.NodeOrientation; 59 import javafx.geometry.Point2D; 60 import javafx.geometry.Pos; 61 import javafx.geometry.Side; 62 import javafx.geometry.VPos; 63 import javafx.scene.AccessibleAction; 64 import javafx.scene.AccessibleAttribute; 65 import javafx.scene.AccessibleRole; 66 import javafx.scene.Node; 67 import javafx.scene.control.ContextMenu; 68 import javafx.scene.control.Control; 69 import javafx.scene.control.Label; 70 import javafx.scene.control.MenuItem; 71 import javafx.scene.control.RadioMenuItem; 72 import javafx.scene.control.SkinBase; 73 import javafx.scene.control.Tab; 74 import javafx.scene.control.TabPane; 75 import javafx.scene.control.TabPane.TabClosingPolicy; 76 import javafx.scene.control.TabPane.TabDragPolicy; 77 import javafx.scene.control.ToggleGroup; 78 import javafx.scene.control.Tooltip; 79 import javafx.scene.effect.DropShadow; 80 import javafx.scene.image.ImageView; 81 import javafx.scene.input.ContextMenuEvent; 82 import javafx.scene.input.MouseButton; 83 import javafx.scene.input.MouseEvent; 84 import javafx.scene.input.ScrollEvent; 85 import javafx.scene.input.SwipeEvent; 86 import javafx.scene.layout.Pane; 87 import javafx.scene.layout.Region; 88 import javafx.scene.layout.StackPane; 89 import javafx.scene.shape.Rectangle; 90 import javafx.scene.transform.Rotate; 91 import javafx.util.Duration; 92 93 import java.util.ArrayList; 94 import java.util.Collections; 95 import java.util.Iterator; 96 import java.util.List; 97 98 import javafx.css.converter.EnumConverter; 99 import com.sun.javafx.scene.control.behavior.TabPaneBehavior; 100 101 import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString; 102 103 /** 104 * Default skin implementation for the {@link TabPane} control. 105 * 106 * @see TabPane 107 * @since 9 108 */ 109 public class TabPaneSkin extends SkinBase<TabPane> { 110 111 /*************************************************************************** 112 * * 113 * Enums * 114 * * 115 **************************************************************************/ 116 117 private enum TabAnimation { 118 NONE, 119 GROW 120 // In future we could add FADE, ... 121 } 122 123 private enum TabAnimationState { 124 SHOWING, HIDING, NONE; 125 } 126 127 128 129 /*************************************************************************** 130 * * 131 * Static fields * 132 * * 133 **************************************************************************/ 134 135 static int CLOSE_BTN_SIZE = 16; 136 137 138 139 /*************************************************************************** 140 * * 141 * Private fields * 142 * * 143 **************************************************************************/ 144 145 private static final double ANIMATION_SPEED = 150; 146 private static final int SPACER = 10; 147 148 private TabHeaderArea tabHeaderArea; 149 private ObservableList<TabContentRegion> tabContentRegions; 150 private Rectangle clipRect; 151 private Rectangle tabHeaderAreaClipRect; 152 private Tab selectedTab; 153 private boolean isSelectingTab; 154 155 private final TabPaneBehavior behavior; 156 157 158 159 /*************************************************************************** 160 * * 161 * Constructors * 162 * * 163 **************************************************************************/ 164 165 /** 166 * Creates a new TabPaneSkin instance, installing the necessary child 167 * nodes into the Control {@link Control#getChildren() children} list, as 168 * well as the necessary input mappings for handling key, mouse, etc events. 169 * 170 * @param control The control that this skin should be installed onto. 171 */ 172 public TabPaneSkin(TabPane control) { 173 super(control); 174 175 // install default input map for the TabPane control 176 this.behavior = new TabPaneBehavior(control); 177 // control.setInputMap(behavior.getInputMap()); 178 179 clipRect = new Rectangle(control.getWidth(), control.getHeight()); 180 getSkinnable().setClip(clipRect); 181 182 tabContentRegions = FXCollections.<TabContentRegion>observableArrayList(); 183 184 for (Tab tab : getSkinnable().getTabs()) { 185 addTabContent(tab); 186 } 187 188 tabHeaderAreaClipRect = new Rectangle(); 189 tabHeaderArea = new TabHeaderArea(); 190 tabHeaderArea.setClip(tabHeaderAreaClipRect); 191 getChildren().add(tabHeaderArea); 192 if (getSkinnable().getTabs().size() == 0) { 193 tabHeaderArea.setVisible(false); 194 } 195 196 initializeTabListener(); 197 198 registerChangeListener(control.getSelectionModel().selectedItemProperty(), e -> { 199 isSelectingTab = true; 200 selectedTab = getSkinnable().getSelectionModel().getSelectedItem(); 201 getSkinnable().requestLayout(); 202 }); 203 registerChangeListener(control.sideProperty(), e -> updateTabPosition()); 204 registerChangeListener(control.widthProperty(), e -> clipRect.setWidth(getSkinnable().getWidth())); 205 registerChangeListener(control.heightProperty(), e -> clipRect.setHeight(getSkinnable().getHeight())); 206 207 selectedTab = getSkinnable().getSelectionModel().getSelectedItem(); 208 // Could not find the selected tab try and get the selected tab using the selected index 209 if (selectedTab == null && getSkinnable().getSelectionModel().getSelectedIndex() != -1) { 210 getSkinnable().getSelectionModel().select(getSkinnable().getSelectionModel().getSelectedIndex()); 211 selectedTab = getSkinnable().getSelectionModel().getSelectedItem(); 212 } 213 if (selectedTab == null) { 214 // getSelectedItem and getSelectedIndex failed select the first. 215 getSkinnable().getSelectionModel().selectFirst(); 216 } 217 selectedTab = getSkinnable().getSelectionModel().getSelectedItem(); 218 isSelectingTab = false; 219 220 initializeSwipeHandlers(); 221 } 222 223 224 225 /*************************************************************************** 226 * * 227 * Properties * 228 * * 229 **************************************************************************/ 230 231 private ObjectProperty<TabAnimation> openTabAnimation = new StyleableObjectProperty<TabAnimation>(TabAnimation.GROW) { 232 @Override public CssMetaData<TabPane,TabAnimation> getCssMetaData() { 233 return StyleableProperties.OPEN_TAB_ANIMATION; 234 } 235 236 @Override public Object getBean() { 237 return TabPaneSkin.this; 238 } 239 240 @Override public String getName() { 241 return "openTabAnimation"; 242 } 243 }; 244 245 private ObjectProperty<TabAnimation> closeTabAnimation = new StyleableObjectProperty<TabAnimation>(TabAnimation.GROW) { 246 @Override public CssMetaData<TabPane,TabAnimation> getCssMetaData() { 247 return StyleableProperties.CLOSE_TAB_ANIMATION; 248 } 249 250 @Override public Object getBean() { 251 return TabPaneSkin.this; 252 } 253 254 @Override public String getName() { 255 return "closeTabAnimation"; 256 } 257 }; 258 259 260 261 /*************************************************************************** 262 * * 263 * Public API * 264 * * 265 **************************************************************************/ 266 267 /** {@inheritDoc} */ 268 @Override public void dispose() { 269 super.dispose(); 270 271 if (behavior != null) { 272 behavior.dispose(); 273 } 274 } 275 276 /** {@inheritDoc} */ 277 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 278 // The TabPane can only be as wide as it widest content width. 279 double maxw = 0.0; 280 for (TabContentRegion contentRegion: tabContentRegions) { 281 maxw = Math.max(maxw, snapSizeX(contentRegion.prefWidth(-1))); 282 } 283 284 final boolean isHorizontal = isHorizontal(); 285 final double tabHeaderAreaSize = isHorizontal 286 ? snapSizeX(tabHeaderArea.prefWidth(-1)) 287 : snapSizeY(tabHeaderArea.prefHeight(-1)); 288 289 double prefWidth = isHorizontal ? 290 Math.max(maxw, tabHeaderAreaSize) : maxw + tabHeaderAreaSize; 291 return snapSizeX(prefWidth) + rightInset + leftInset; 292 } 293 294 /** {@inheritDoc} */ 295 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 296 // The TabPane can only be as high as it highest content height. 297 double maxh = 0.0; 298 for (TabContentRegion contentRegion: tabContentRegions) { 299 maxh = Math.max(maxh, snapSizeY(contentRegion.prefHeight(-1))); 300 } 301 302 final boolean isHorizontal = isHorizontal(); 303 final double tabHeaderAreaSize = isHorizontal 304 ? snapSizeY(tabHeaderArea.prefHeight(-1)) 305 : snapSizeX(tabHeaderArea.prefWidth(-1)); 306 307 double prefHeight = isHorizontal ? 308 maxh + snapSizeY(tabHeaderAreaSize) : Math.max(maxh, tabHeaderAreaSize); 309 return snapSizeY(prefHeight) + topInset + bottomInset; 310 } 311 312 /** {@inheritDoc} */ 313 @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) { 314 Side tabPosition = getSkinnable().getSide(); 315 if (tabPosition == Side.TOP) { 316 return tabHeaderArea.getBaselineOffset() + topInset; 317 } 318 return 0; 319 } 320 321 /** {@inheritDoc} */ 322 @Override protected void layoutChildren(final double x, final double y, 323 final double w, final double h) { 324 TabPane tabPane = getSkinnable(); 325 Side tabPosition = tabPane.getSide(); 326 327 double headerHeight = tabPosition.isHorizontal() 328 ? snapSizeY(tabHeaderArea.prefHeight(-1)) 329 : snapSizeX(tabHeaderArea.prefHeight(-1)); 330 double tabsStartX = tabPosition.equals(Side.RIGHT)? x + w - headerHeight : x; 331 double tabsStartY = tabPosition.equals(Side.BOTTOM)? y + h - headerHeight : y; 332 333 final double leftInset = snappedLeftInset(); 334 final double topInset = snappedTopInset(); 335 336 if (tabPosition == Side.TOP) { 337 tabHeaderArea.resize(w, headerHeight); 338 tabHeaderArea.relocate(tabsStartX, tabsStartY); 339 tabHeaderArea.getTransforms().clear(); 340 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.TOP))); 341 } else if (tabPosition == Side.BOTTOM) { 342 tabHeaderArea.resize(w, headerHeight); 343 tabHeaderArea.relocate(w + leftInset, tabsStartY - headerHeight); 344 tabHeaderArea.getTransforms().clear(); 345 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.BOTTOM), 0, headerHeight)); 346 } else if (tabPosition == Side.LEFT) { 347 tabHeaderArea.resize(h, headerHeight); 348 tabHeaderArea.relocate(tabsStartX + headerHeight, h - headerHeight + topInset); 349 tabHeaderArea.getTransforms().clear(); 350 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.LEFT), 0, headerHeight)); 351 } else if (tabPosition == Side.RIGHT) { 352 tabHeaderArea.resize(h, headerHeight); 353 tabHeaderArea.relocate(tabsStartX, y - headerHeight); 354 tabHeaderArea.getTransforms().clear(); 355 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.RIGHT), 0, headerHeight)); 356 } 357 358 tabHeaderAreaClipRect.setX(0); 359 tabHeaderAreaClipRect.setY(0); 360 if (isHorizontal()) { 361 tabHeaderAreaClipRect.setWidth(w); 362 } else { 363 tabHeaderAreaClipRect.setWidth(h); 364 } 365 tabHeaderAreaClipRect.setHeight(headerHeight); 366 367 // ================================== 368 // position the tab content for the selected tab only 369 // ================================== 370 // if the tabs are on the left, the content needs to be indented 371 double contentStartX = 0; 372 double contentStartY = 0; 373 374 if (tabPosition == Side.TOP) { 375 contentStartX = x; 376 contentStartY = y + headerHeight; 377 if (isFloatingStyleClass()) { 378 // This is to hide the top border content 379 contentStartY -= 1; 380 } 381 } else if (tabPosition == Side.BOTTOM) { 382 contentStartX = x; 383 contentStartY = y + topInset; 384 if (isFloatingStyleClass()) { 385 // This is to hide the bottom border content 386 contentStartY = 1 + topInset; 387 } 388 } else if (tabPosition == Side.LEFT) { 389 contentStartX = x + headerHeight; 390 contentStartY = y; 391 if (isFloatingStyleClass()) { 392 // This is to hide the left border content 393 contentStartX -= 1; 394 } 395 } else if (tabPosition == Side.RIGHT) { 396 contentStartX = x + leftInset; 397 contentStartY = y; 398 if (isFloatingStyleClass()) { 399 // This is to hide the right border content 400 contentStartX = 1 + leftInset; 401 } 402 } 403 404 double contentWidth = w - (isHorizontal() ? 0 : headerHeight); 405 double contentHeight = h - (isHorizontal() ? headerHeight: 0); 406 407 for (int i = 0, max = tabContentRegions.size(); i < max; i++) { 408 TabContentRegion tabContent = tabContentRegions.get(i); 409 410 tabContent.setAlignment(Pos.TOP_LEFT); 411 if (tabContent.getClip() != null) { 412 ((Rectangle)tabContent.getClip()).setWidth(contentWidth); 413 ((Rectangle)tabContent.getClip()).setHeight(contentHeight); 414 } 415 416 // we need to size all tabs, even if they aren't visible. For example, 417 // see RT-29167 418 tabContent.resize(contentWidth, contentHeight); 419 tabContent.relocate(contentStartX, contentStartY); 420 } 421 } 422 423 424 425 /*************************************************************************** 426 * * 427 * Private implementation * 428 * * 429 **************************************************************************/ 430 431 private static int getRotation(Side pos) { 432 switch (pos) { 433 case TOP: 434 return 0; 435 case BOTTOM: 436 return 180; 437 case LEFT: 438 return -90; 439 case RIGHT: 440 return 90; 441 default: 442 return 0; 443 } 444 } 445 446 /** 447 * VERY HACKY - this lets us 'duplicate' Label and ImageView nodes to be used in a 448 * Tab and the tabs menu at the same time. 449 */ 450 private static Node clone(Node n) { 451 if (n == null) { 452 return null; 453 } 454 if (n instanceof ImageView) { 455 ImageView iv = (ImageView) n; 456 ImageView imageview = new ImageView(); 457 imageview.imageProperty().bind(iv.imageProperty()); 458 return imageview; 459 } 460 if (n instanceof Label) { 461 Label l = (Label)n; 462 Label label = new Label(l.getText(), clone(l.getGraphic())); 463 label.textProperty().bind(l.textProperty()); 464 return label; 465 } 466 return null; 467 } 468 469 private void removeTabs(List<? extends Tab> removedList) { 470 for (final Tab tab : removedList) { 471 stopCurrentAnimation(tab); 472 // Animate the tab removal 473 final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab); 474 if (tabRegion != null) { 475 tabRegion.isClosing = true; 476 477 tabRegion.removeListeners(tab); 478 removeTabContent(tab); 479 480 // remove the menu item from the popup menu 481 ContextMenu popupMenu = tabHeaderArea.controlButtons.popup; 482 TabMenuItem tabItem = null; 483 if (popupMenu != null) { 484 for (MenuItem item : popupMenu.getItems()) { 485 tabItem = (TabMenuItem) item; 486 if (tab == tabItem.getTab()) { 487 break; 488 } 489 tabItem = null; 490 } 491 } 492 if (tabItem != null) { 493 tabItem.dispose(); 494 popupMenu.getItems().remove(tabItem); 495 } 496 // end of removing menu item 497 498 EventHandler<ActionEvent> cleanup = ae -> { 499 tabRegion.animationState = TabAnimationState.NONE; 500 501 tabHeaderArea.removeTab(tab); 502 tabHeaderArea.requestLayout(); 503 if (getSkinnable().getTabs().isEmpty()) { 504 tabHeaderArea.setVisible(false); 505 } 506 }; 507 508 if (closeTabAnimation.get() == TabAnimation.GROW) { 509 tabRegion.animationState = TabAnimationState.HIDING; 510 Timeline closedTabTimeline = tabRegion.currentAnimation = 511 createTimeline(tabRegion, Duration.millis(ANIMATION_SPEED), 0.0F, cleanup); 512 closedTabTimeline.play(); 513 } else { 514 cleanup.handle(null); 515 } 516 } 517 } 518 } 519 520 private void stopCurrentAnimation(Tab tab) { 521 final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab); 522 if (tabRegion != null) { 523 // Execute the code immediately, don't wait for the animation to finish. 524 Timeline timeline = tabRegion.currentAnimation; 525 if (timeline != null && timeline.getStatus() == Animation.Status.RUNNING) { 526 timeline.getOnFinished().handle(null); 527 timeline.stop(); 528 tabRegion.currentAnimation = null; 529 } 530 } 531 } 532 533 private void addTabs(List<? extends Tab> addedList, int from) { 534 int i = 0; 535 536 // RT-39984: check if any other tabs are animating - they must be completed first. 537 List<Node> headers = new ArrayList<>(tabHeaderArea.headersRegion.getChildren()); 538 for (Node n : headers) { 539 TabHeaderSkin header = (TabHeaderSkin) n; 540 if (header.animationState == TabAnimationState.HIDING) { 541 stopCurrentAnimation(header.tab); 542 } 543 } 544 // end of fix for RT-39984 545 546 for (final Tab tab : addedList) { 547 stopCurrentAnimation(tab); // Note that this must happen before addTab() call below 548 // A new tab was added - animate it out 549 if (!tabHeaderArea.isVisible()) { 550 tabHeaderArea.setVisible(true); 551 } 552 int index = from + i++; 553 tabHeaderArea.addTab(tab, index); 554 addTabContent(tab); 555 final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab); 556 if (tabRegion != null) { 557 if (openTabAnimation.get() == TabAnimation.GROW) { 558 tabRegion.animationState = TabAnimationState.SHOWING; 559 tabRegion.animationTransition.setValue(0.0); 560 tabRegion.setVisible(true); 561 tabRegion.currentAnimation = createTimeline(tabRegion, Duration.millis(ANIMATION_SPEED), 1.0, event -> { 562 tabRegion.animationState = TabAnimationState.NONE; 563 tabRegion.setVisible(true); 564 tabRegion.inner.requestLayout(); 565 }); 566 tabRegion.currentAnimation.play(); 567 } else { 568 tabRegion.setVisible(true); 569 tabRegion.inner.requestLayout(); 570 } 571 } 572 } 573 } 574 575 private void initializeTabListener() { 576 getSkinnable().getTabs().addListener((ListChangeListener<Tab>) c -> { 577 List<Tab> tabsToRemove = new ArrayList<>(); 578 List<Tab> tabsToAdd = new ArrayList<>(); 579 int insertPos = -1; 580 581 while (c.next()) { 582 if (c.wasPermutated()) { 583 if (dragState != DragState.REORDER) { 584 TabPane tabPane = getSkinnable(); 585 List<Tab> tabs = tabPane.getTabs(); 586 587 // tabs sorted : create list of permutated tabs. 588 // clear selection, set tab animation to NONE 589 // remove permutated tabs, add them back in correct order. 590 // restore old selection, and old tab animation states. 591 int size = c.getTo() - c.getFrom(); 592 Tab selTab = tabPane.getSelectionModel().getSelectedItem(); 593 List<Tab> permutatedTabs = new ArrayList<Tab>(size); 594 getSkinnable().getSelectionModel().clearSelection(); 595 596 // save and set tab animation to none - as it is not a good idea 597 // to animate on the same data for open and close. 598 TabAnimation prevOpenAnimation = openTabAnimation.get(); 599 TabAnimation prevCloseAnimation = closeTabAnimation.get(); 600 openTabAnimation.set(TabAnimation.NONE); 601 closeTabAnimation.set(TabAnimation.NONE); 602 for (int i = c.getFrom(); i < c.getTo(); i++) { 603 permutatedTabs.add(tabs.get(i)); 604 } 605 606 removeTabs(permutatedTabs); 607 addTabs(permutatedTabs, c.getFrom()); 608 openTabAnimation.set(prevOpenAnimation); 609 closeTabAnimation.set(prevCloseAnimation); 610 getSkinnable().getSelectionModel().select(selTab); 611 } 612 } 613 614 if (c.wasRemoved()) { 615 tabsToRemove.addAll(c.getRemoved()); 616 } 617 if (c.wasAdded()) { 618 tabsToAdd.addAll(c.getAddedSubList()); 619 insertPos = c.getFrom(); 620 } 621 } 622 623 // now only remove the tabs that are not in the tabsToAdd list 624 tabsToRemove.removeAll(tabsToAdd); 625 removeTabs(tabsToRemove); 626 627 // and add in any new tabs (that we don't already have showing) 628 if (!tabsToAdd.isEmpty()) { 629 for (TabContentRegion tabContentRegion : tabContentRegions) { 630 Tab tab = tabContentRegion.getTab(); 631 TabHeaderSkin tabHeader = tabHeaderArea.getTabHeaderSkin(tab); 632 if (!tabHeader.isClosing && tabsToAdd.contains(tabContentRegion.getTab())) { 633 tabsToAdd.remove(tabContentRegion.getTab()); 634 } 635 } 636 637 addTabs(tabsToAdd, insertPos == -1 ? tabContentRegions.size() : insertPos); 638 } 639 640 // Fix for RT-34692 641 getSkinnable().requestLayout(); 642 }); 643 } 644 645 private void addTabContent(Tab tab) { 646 TabContentRegion tabContentRegion = new TabContentRegion(tab); 647 tabContentRegion.setClip(new Rectangle()); 648 tabContentRegions.add(tabContentRegion); 649 // We want the tab content to always sit below the tab headers 650 getChildren().add(0, tabContentRegion); 651 } 652 653 private void removeTabContent(Tab tab) { 654 for (TabContentRegion contentRegion : tabContentRegions) { 655 if (contentRegion.getTab().equals(tab)) { 656 contentRegion.removeListeners(tab); 657 getChildren().remove(contentRegion); 658 tabContentRegions.remove(contentRegion); 659 break; 660 } 661 } 662 } 663 664 private void updateTabPosition() { 665 tabHeaderArea.setScrollOffset(0.0F); 666 getSkinnable().applyCss(); 667 getSkinnable().requestLayout(); 668 } 669 670 private Timeline createTimeline(final TabHeaderSkin tabRegion, final Duration duration, final double endValue, final EventHandler<ActionEvent> func) { 671 Timeline timeline = new Timeline(); 672 timeline.setCycleCount(1); 673 674 KeyValue keyValue = new KeyValue(tabRegion.animationTransition, endValue, Interpolator.LINEAR); 675 timeline.getKeyFrames().clear(); 676 timeline.getKeyFrames().add(new KeyFrame(duration, keyValue)); 677 678 timeline.setOnFinished(func); 679 return timeline; 680 } 681 682 private boolean isHorizontal() { 683 Side tabPosition = getSkinnable().getSide(); 684 return Side.TOP.equals(tabPosition) || Side.BOTTOM.equals(tabPosition); 685 } 686 687 private void initializeSwipeHandlers() { 688 if (Properties.IS_TOUCH_SUPPORTED) { 689 getSkinnable().addEventHandler(SwipeEvent.SWIPE_LEFT, t -> { 690 behavior.selectNextTab(); 691 }); 692 693 getSkinnable().addEventHandler(SwipeEvent.SWIPE_RIGHT, t -> { 694 behavior.selectPreviousTab(); 695 }); 696 } 697 } 698 699 //TODO need to cache this. 700 private boolean isFloatingStyleClass() { 701 return getSkinnable().getStyleClass().contains(TabPane.STYLE_CLASS_FLOATING); 702 } 703 704 705 706 /*************************************************************************** 707 * * 708 * CSS * 709 * * 710 **************************************************************************/ 711 712 /* 713 * Super-lazy instantiation pattern from Bill Pugh. 714 */ 715 private static class StyleableProperties { 716 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 717 718 private final static CssMetaData<TabPane,TabAnimation> OPEN_TAB_ANIMATION = 719 new CssMetaData<TabPane, TabPaneSkin.TabAnimation>("-fx-open-tab-animation", 720 new EnumConverter<TabAnimation>(TabAnimation.class), TabAnimation.GROW) { 721 722 @Override public boolean isSettable(TabPane node) { 723 return true; 724 } 725 726 @Override public StyleableProperty<TabAnimation> getStyleableProperty(TabPane node) { 727 TabPaneSkin skin = (TabPaneSkin) node.getSkin(); 728 return (StyleableProperty<TabAnimation>)(WritableValue<TabAnimation>)skin.openTabAnimation; 729 } 730 }; 731 732 private final static CssMetaData<TabPane,TabAnimation> CLOSE_TAB_ANIMATION = 733 new CssMetaData<TabPane, TabPaneSkin.TabAnimation>("-fx-close-tab-animation", 734 new EnumConverter<TabAnimation>(TabAnimation.class), TabAnimation.GROW) { 735 736 @Override public boolean isSettable(TabPane node) { 737 return true; 738 } 739 740 @Override public StyleableProperty<TabAnimation> getStyleableProperty(TabPane node) { 741 TabPaneSkin skin = (TabPaneSkin) node.getSkin(); 742 return (StyleableProperty<TabAnimation>)(WritableValue<TabAnimation>)skin.closeTabAnimation; 743 } 744 }; 745 746 static { 747 748 final List<CssMetaData<? extends Styleable, ?>> styleables = 749 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData()); 750 styleables.add(OPEN_TAB_ANIMATION); 751 styleables.add(CLOSE_TAB_ANIMATION); 752 STYLEABLES = Collections.unmodifiableList(styleables); 753 754 } 755 } 756 757 /** 758 * Returns the CssMetaData associated with this class, which may include the 759 * CssMetaData of its superclasses. 760 * @return the CssMetaData associated with this class, which may include the 761 * CssMetaData of its superclasses 762 */ 763 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 764 return StyleableProperties.STYLEABLES; 765 } 766 767 /** 768 * {@inheritDoc} 769 */ 770 @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 771 return getClassCssMetaData(); 772 } 773 774 775 776 /*************************************************************************** 777 * * 778 * Support classes * 779 * * 780 **************************************************************************/ 781 782 /************************************************************************** 783 * 784 * TabHeaderArea: Area responsible for painting all tabs 785 * 786 **************************************************************************/ 787 class TabHeaderArea extends StackPane { 788 private Rectangle headerClip; 789 private StackPane headersRegion; 790 private StackPane headerBackground; 791 private TabControlButtons controlButtons; 792 793 private boolean measureClosingTabs = false; 794 795 private double scrollOffset; 796 797 public TabHeaderArea() { 798 getStyleClass().setAll("tab-header-area"); 799 setManaged(false); 800 final TabPane tabPane = getSkinnable(); 801 802 headerClip = new Rectangle(); 803 804 headersRegion = new StackPane() { 805 @Override protected double computePrefWidth(double height) { 806 double width = 0.0F; 807 for (Node child : getChildren()) { 808 TabHeaderSkin tabHeaderSkin = (TabHeaderSkin)child; 809 if (tabHeaderSkin.isVisible() && (measureClosingTabs || ! tabHeaderSkin.isClosing)) { 810 width += tabHeaderSkin.prefWidth(height); 811 } 812 } 813 return snapSize(width) + snappedLeftInset() + snappedRightInset(); 814 } 815 816 @Override protected double computePrefHeight(double width) { 817 double height = 0.0F; 818 for (Node child : getChildren()) { 819 TabHeaderSkin tabHeaderSkin = (TabHeaderSkin)child; 820 height = Math.max(height, tabHeaderSkin.prefHeight(width)); 821 } 822 return snapSize(height) + snappedTopInset() + snappedBottomInset(); 823 } 824 825 @Override protected void layoutChildren() { 826 if (tabsFit()) { 827 setScrollOffset(0.0); 828 } else { 829 if (!removeTab.isEmpty()) { 830 double offset = 0; 831 double w = tabHeaderArea.getWidth() - snapSize(controlButtons.prefWidth(-1)) - firstTabIndent() - SPACER; 832 Iterator<Node> i = getChildren().iterator(); 833 while (i.hasNext()) { 834 TabHeaderSkin tabHeader = (TabHeaderSkin)i.next(); 835 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1)); 836 if (removeTab.contains(tabHeader)) { 837 if (offset < w) { 838 isSelectingTab = true; 839 } 840 i.remove(); 841 removeTab.remove(tabHeader); 842 if (removeTab.isEmpty()) { 843 break; 844 } 845 } 846 offset += tabHeaderPrefWidth; 847 } 848 // } else { 849 // isSelectingTab = true; 850 } 851 } 852 853 if (isSelectingTab) { 854 ensureSelectedTabIsVisible(); 855 isSelectingTab = false; 856 } else { 857 validateScrollOffset(); 858 } 859 860 Side tabPosition = getSkinnable().getSide(); 861 double tabBackgroundHeight = snapSize(prefHeight(-1)); 862 double tabX = (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) ? 863 snapSize(getWidth()) - getScrollOffset() : getScrollOffset(); 864 865 updateHeaderClip(); 866 for (Node node : getChildren()) { 867 TabHeaderSkin tabHeader = (TabHeaderSkin)node; 868 869 // size and position the header relative to the other headers 870 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1) * tabHeader.animationTransition.get()); 871 double tabHeaderPrefHeight = snapSize(tabHeader.prefHeight(-1)); 872 tabHeader.resize(tabHeaderPrefWidth, tabHeaderPrefHeight); 873 874 // This ensures that the tabs are located in the correct position 875 // when there are tabs of differing heights. 876 double startY = tabPosition.equals(Side.BOTTOM) ? 877 0 : tabBackgroundHeight - tabHeaderPrefHeight - snappedBottomInset(); 878 if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) { 879 // build from the right 880 tabX -= tabHeaderPrefWidth; 881 if (dragState != DragState.REORDER || 882 (tabHeader != dragTabHeader && tabHeader != dropAnimHeader)) { 883 tabHeader.relocate(tabX, startY); 884 } 885 } else { 886 // build from the left 887 if (dragState != DragState.REORDER || 888 (tabHeader != dragTabHeader && tabHeader != dropAnimHeader)) { 889 tabHeader.relocate(tabX, startY); 890 } 891 tabX += tabHeaderPrefWidth; 892 } 893 } 894 } 895 896 }; 897 headersRegion.getStyleClass().setAll("headers-region"); 898 headersRegion.setClip(headerClip); 899 setupReordering(headersRegion); 900 901 headerBackground = new StackPane(); 902 headerBackground.getStyleClass().setAll("tab-header-background"); 903 904 int i = 0; 905 for (Tab tab: tabPane.getTabs()) { 906 addTab(tab, i++); 907 } 908 909 controlButtons = new TabControlButtons(); 910 controlButtons.setVisible(false); 911 if (controlButtons.isVisible()) { 912 controlButtons.setVisible(true); 913 } 914 getChildren().addAll(headerBackground, headersRegion, controlButtons); 915 916 // support for mouse scroll of header area (for when the tabs exceed 917 // the available space). 918 // Scrolling the mouse wheel downwards results in the tabs scrolling left (i.e. exposing the right-most tabs) 919 // Scrolling the mouse wheel upwards results in the tabs scrolling right (i.e. exposing th left-most tabs) 920 addEventHandler(ScrollEvent.SCROLL, (ScrollEvent e) -> { 921 Side side = getSkinnable().getSide(); 922 side = side == null ? Side.TOP : side; 923 switch (side) { 924 default: 925 case TOP: 926 case BOTTOM: 927 setScrollOffset(scrollOffset + e.getDeltaY()); 928 break; 929 case LEFT: 930 case RIGHT: 931 setScrollOffset(scrollOffset - e.getDeltaY()); 932 break; 933 } 934 935 }); 936 } 937 938 private void updateHeaderClip() { 939 Side tabPosition = getSkinnable().getSide(); 940 941 double x = 0; 942 double y = 0; 943 double clipWidth = 0; 944 double clipHeight = 0; 945 double maxWidth = 0; 946 double shadowRadius = 0; 947 double clipOffset = firstTabIndent(); 948 double controlButtonPrefWidth = snapSize(controlButtons.prefWidth(-1)); 949 950 measureClosingTabs = true; 951 double headersPrefWidth = snapSize(headersRegion.prefWidth(-1)); 952 measureClosingTabs = false; 953 954 double headersPrefHeight = snapSize(headersRegion.prefHeight(-1)); 955 956 // Add the spacer if isShowTabsMenu is true. 957 if (controlButtonPrefWidth > 0) { 958 controlButtonPrefWidth = controlButtonPrefWidth + SPACER; 959 } 960 961 if (headersRegion.getEffect() instanceof DropShadow) { 962 DropShadow shadow = (DropShadow)headersRegion.getEffect(); 963 shadowRadius = shadow.getRadius(); 964 } 965 966 maxWidth = snapSize(getWidth()) - controlButtonPrefWidth - clipOffset; 967 if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) { 968 if (headersPrefWidth < maxWidth) { 969 clipWidth = headersPrefWidth + shadowRadius; 970 } else { 971 x = headersPrefWidth - maxWidth; 972 clipWidth = maxWidth + shadowRadius; 973 } 974 clipHeight = headersPrefHeight; 975 } else { 976 // If x = 0 the header region's drop shadow is clipped. 977 x = -shadowRadius; 978 clipWidth = (headersPrefWidth < maxWidth ? headersPrefWidth : maxWidth) + shadowRadius; 979 clipHeight = headersPrefHeight; 980 } 981 982 headerClip.setX(x); 983 headerClip.setY(y); 984 headerClip.setWidth(clipWidth); 985 headerClip.setHeight(clipHeight); 986 } 987 988 private void addTab(Tab tab, int addToIndex) { 989 TabHeaderSkin tabHeaderSkin = new TabHeaderSkin(tab); 990 headersRegion.getChildren().add(addToIndex, tabHeaderSkin); 991 } 992 993 private List<TabHeaderSkin> removeTab = new ArrayList<>(); 994 private void removeTab(Tab tab) { 995 TabHeaderSkin tabHeaderSkin = getTabHeaderSkin(tab); 996 if (tabHeaderSkin != null) { 997 if (tabsFit()) { 998 headersRegion.getChildren().remove(tabHeaderSkin); 999 } else { 1000 // The tab will be removed during layout because 1001 // we need its width to compute the scroll offset. 1002 removeTab.add(tabHeaderSkin); 1003 tabHeaderSkin.removeListeners(tab); 1004 } 1005 } 1006 } 1007 1008 private TabHeaderSkin getTabHeaderSkin(Tab tab) { 1009 for (Node child: headersRegion.getChildren()) { 1010 TabHeaderSkin tabHeaderSkin = (TabHeaderSkin)child; 1011 if (tabHeaderSkin.getTab().equals(tab)) { 1012 return tabHeaderSkin; 1013 } 1014 } 1015 return null; 1016 } 1017 1018 private boolean tabsFit() { 1019 double headerPrefWidth = snapSize(headersRegion.prefWidth(-1)); 1020 double controlTabWidth = snapSize(controlButtons.prefWidth(-1)); 1021 double visibleWidth = headerPrefWidth + controlTabWidth + firstTabIndent() + SPACER; 1022 return visibleWidth < getWidth(); 1023 } 1024 1025 private void ensureSelectedTabIsVisible() { 1026 // work out the visible width of the tab header 1027 double tabPaneWidth = snapSize(isHorizontal() ? getSkinnable().getWidth() : getSkinnable().getHeight()); 1028 double controlTabWidth = snapSize(controlButtons.getWidth()); 1029 double visibleWidth = tabPaneWidth - controlTabWidth - firstTabIndent() - SPACER; 1030 1031 // and get where the selected tab is in the header area 1032 double offset = 0.0; 1033 double selectedTabOffset = 0.0; 1034 double selectedTabWidth = 0.0; 1035 for (Node node : headersRegion.getChildren()) { 1036 TabHeaderSkin tabHeader = (TabHeaderSkin)node; 1037 1038 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1)); 1039 1040 if (selectedTab != null && selectedTab.equals(tabHeader.getTab())) { 1041 selectedTabOffset = offset; 1042 selectedTabWidth = tabHeaderPrefWidth; 1043 } 1044 offset += tabHeaderPrefWidth; 1045 } 1046 1047 final double scrollOffset = getScrollOffset(); 1048 final double selectedTabStartX = selectedTabOffset; 1049 final double selectedTabEndX = selectedTabOffset + selectedTabWidth; 1050 1051 final double visibleAreaEndX = visibleWidth; 1052 1053 if (selectedTabStartX < -scrollOffset) { 1054 setScrollOffset(-selectedTabStartX); 1055 } else if (selectedTabEndX > (visibleAreaEndX - scrollOffset)) { 1056 setScrollOffset(visibleAreaEndX - selectedTabEndX); 1057 } 1058 } 1059 1060 public double getScrollOffset() { 1061 return scrollOffset; 1062 } 1063 1064 private void validateScrollOffset() { 1065 setScrollOffset(getScrollOffset()); 1066 } 1067 1068 private void setScrollOffset(double newScrollOffset) { 1069 // work out the visible width of the tab header 1070 double tabPaneWidth = snapSize(isHorizontal() ? getSkinnable().getWidth() : getSkinnable().getHeight()); 1071 double controlTabWidth = snapSize(controlButtons.getWidth()); 1072 double visibleWidth = tabPaneWidth - controlTabWidth - firstTabIndent() - SPACER; 1073 1074 // measure the width of all tabs 1075 double offset = 0.0; 1076 for (Node node : headersRegion.getChildren()) { 1077 TabHeaderSkin tabHeader = (TabHeaderSkin)node; 1078 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1)); 1079 offset += tabHeaderPrefWidth; 1080 } 1081 1082 double actualNewScrollOffset; 1083 1084 if ((visibleWidth - newScrollOffset) > offset && newScrollOffset < 0) { 1085 // need to make sure the right-most tab is attached to the 1086 // right-hand side of the tab header (e.g. if the tab header area width 1087 // is expanded), and if it isn't modify the scroll offset to bring 1088 // it into line. See RT-35194 for a test case. 1089 actualNewScrollOffset = visibleWidth - offset; 1090 } else if (newScrollOffset > 0) { 1091 // need to prevent the left-most tab from becoming detached 1092 // from the left-hand side of the tab header. 1093 actualNewScrollOffset = 0; 1094 } else { 1095 actualNewScrollOffset = newScrollOffset; 1096 } 1097 1098 if (Math.abs(actualNewScrollOffset - scrollOffset) > 0.001) { 1099 scrollOffset = actualNewScrollOffset; 1100 headersRegion.requestLayout(); 1101 } 1102 } 1103 1104 private double firstTabIndent() { 1105 switch (getSkinnable().getSide()) { 1106 case TOP: 1107 case BOTTOM: 1108 return snappedLeftInset(); 1109 case RIGHT: 1110 case LEFT: 1111 return snappedTopInset(); 1112 default: 1113 return 0; 1114 } 1115 } 1116 1117 @Override protected double computePrefWidth(double height) { 1118 double padding = isHorizontal() ? 1119 snappedLeftInset() + snappedRightInset() : 1120 snappedTopInset() + snappedBottomInset(); 1121 return snapSize(headersRegion.prefWidth(height)) + controlButtons.prefWidth(height) + 1122 firstTabIndent() + SPACER + padding; 1123 } 1124 1125 @Override protected double computePrefHeight(double width) { 1126 double padding = isHorizontal() ? 1127 snappedTopInset() + snappedBottomInset() : 1128 snappedLeftInset() + snappedRightInset(); 1129 return snapSize(headersRegion.prefHeight(-1)) + padding; 1130 } 1131 1132 @Override public double getBaselineOffset() { 1133 if (getSkinnable().getSide() == Side.TOP) { 1134 return headersRegion.getBaselineOffset() + snappedTopInset(); 1135 } 1136 return 0; 1137 } 1138 1139 @Override protected void layoutChildren() { 1140 final double leftInset = snappedLeftInset(); 1141 final double rightInset = snappedRightInset(); 1142 final double topInset = snappedTopInset(); 1143 final double bottomInset = snappedBottomInset(); 1144 double w = snapSize(getWidth()) - (isHorizontal() ? 1145 leftInset + rightInset : topInset + bottomInset); 1146 double h = snapSize(getHeight()) - (isHorizontal() ? 1147 topInset + bottomInset : leftInset + rightInset); 1148 double tabBackgroundHeight = snapSize(prefHeight(-1)); 1149 double headersPrefWidth = snapSize(headersRegion.prefWidth(-1)); 1150 double headersPrefHeight = snapSize(headersRegion.prefHeight(-1)); 1151 1152 controlButtons.showTabsMenu(! tabsFit()); 1153 1154 updateHeaderClip(); 1155 headersRegion.requestLayout(); 1156 1157 // RESIZE CONTROL BUTTONS 1158 double btnWidth = snapSize(controlButtons.prefWidth(-1)); 1159 final double btnHeight = controlButtons.prefHeight(btnWidth); 1160 controlButtons.resize(btnWidth, btnHeight); 1161 1162 // POSITION TABS 1163 headersRegion.resize(headersPrefWidth, headersPrefHeight); 1164 1165 if (isFloatingStyleClass()) { 1166 headerBackground.setVisible(false); 1167 } else { 1168 headerBackground.resize(snapSize(getWidth()), snapSize(getHeight())); 1169 headerBackground.setVisible(true); 1170 } 1171 1172 double startX = 0; 1173 double startY = 0; 1174 double controlStartX = 0; 1175 double controlStartY = 0; 1176 Side tabPosition = getSkinnable().getSide(); 1177 1178 if (tabPosition.equals(Side.TOP)) { 1179 startX = leftInset; 1180 startY = tabBackgroundHeight - headersPrefHeight - bottomInset; 1181 controlStartX = w - btnWidth + leftInset; 1182 controlStartY = snapSize(getHeight()) - btnHeight - bottomInset; 1183 } else if (tabPosition.equals(Side.RIGHT)) { 1184 startX = topInset; 1185 startY = tabBackgroundHeight - headersPrefHeight - leftInset; 1186 controlStartX = w - btnWidth + topInset; 1187 controlStartY = snapSize(getHeight()) - btnHeight - leftInset; 1188 } else if (tabPosition.equals(Side.BOTTOM)) { 1189 startX = snapSize(getWidth()) - headersPrefWidth - leftInset; 1190 startY = tabBackgroundHeight - headersPrefHeight - topInset; 1191 controlStartX = rightInset; 1192 controlStartY = snapSize(getHeight()) - btnHeight - topInset; 1193 } else if (tabPosition.equals(Side.LEFT)) { 1194 startX = snapSize(getWidth()) - headersPrefWidth - topInset; 1195 startY = tabBackgroundHeight - headersPrefHeight - rightInset; 1196 controlStartX = leftInset; 1197 controlStartY = snapSize(getHeight()) - btnHeight - rightInset; 1198 } 1199 if (headerBackground.isVisible()) { 1200 positionInArea(headerBackground, 0, 0, 1201 snapSize(getWidth()), snapSize(getHeight()), /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1202 } 1203 positionInArea(headersRegion, startX, startY, w, h, /*baseline ignored*/0, HPos.LEFT, VPos.CENTER); 1204 positionInArea(controlButtons, controlStartX, controlStartY, btnWidth, btnHeight, 1205 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1206 } 1207 } /* End TabHeaderArea */ 1208 1209 1210 1211 1212 /************************************************************************** 1213 * 1214 * TabHeaderSkin: skin for each tab 1215 * 1216 **************************************************************************/ 1217 1218 class TabHeaderSkin extends StackPane { 1219 private final Tab tab; 1220 public Tab getTab() { 1221 return tab; 1222 } 1223 private Label label; 1224 private StackPane closeBtn; 1225 private StackPane inner; 1226 private Tooltip oldTooltip; 1227 private Tooltip tooltip; 1228 private Rectangle clip; 1229 1230 private boolean isClosing = false; 1231 1232 private LambdaMultiplePropertyChangeListenerHandler listener = new LambdaMultiplePropertyChangeListenerHandler(); 1233 1234 private final ListChangeListener<String> styleClassListener = new ListChangeListener<String>() { 1235 @Override 1236 public void onChanged(Change<? extends String> c) { 1237 getStyleClass().setAll(tab.getStyleClass()); 1238 } 1239 }; 1240 1241 private final WeakListChangeListener<String> weakStyleClassListener = 1242 new WeakListChangeListener<>(styleClassListener); 1243 1244 public TabHeaderSkin(final Tab tab) { 1245 getStyleClass().setAll(tab.getStyleClass()); 1246 setId(tab.getId()); 1247 setStyle(tab.getStyle()); 1248 setAccessibleRole(AccessibleRole.TAB_ITEM); 1249 setViewOrder(1); 1250 1251 this.tab = tab; 1252 clip = new Rectangle(); 1253 setClip(clip); 1254 1255 label = new Label(tab.getText(), tab.getGraphic()); 1256 label.getStyleClass().setAll("tab-label"); 1257 1258 closeBtn = new StackPane() { 1259 @Override protected double computePrefWidth(double h) { 1260 return CLOSE_BTN_SIZE; 1261 } 1262 @Override protected double computePrefHeight(double w) { 1263 return CLOSE_BTN_SIZE; 1264 } 1265 @Override 1266 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 1267 switch (action) { 1268 case FIRE: { 1269 Tab tab = getTab(); 1270 if (behavior.canCloseTab(tab)) { 1271 behavior.closeTab(tab); 1272 setOnMousePressed(null); 1273 } 1274 break; 1275 } 1276 default: super.executeAccessibleAction(action, parameters); 1277 } 1278 } 1279 }; 1280 closeBtn.setAccessibleRole(AccessibleRole.BUTTON); 1281 closeBtn.setAccessibleText(getString("Accessibility.title.TabPane.CloseButton")); 1282 closeBtn.getStyleClass().setAll("tab-close-button"); 1283 closeBtn.setOnMousePressed(new EventHandler<MouseEvent>() { 1284 @Override 1285 public void handle(MouseEvent me) { 1286 Tab tab = getTab(); 1287 if (behavior.canCloseTab(tab)) { 1288 behavior.closeTab(tab); 1289 setOnMousePressed(null); 1290 me.consume(); 1291 } 1292 } 1293 }); 1294 1295 updateGraphicRotation(); 1296 1297 final Region focusIndicator = new Region(); 1298 focusIndicator.setMouseTransparent(true); 1299 focusIndicator.getStyleClass().add("focus-indicator"); 1300 1301 inner = new StackPane() { 1302 @Override protected void layoutChildren() { 1303 final TabPane skinnable = getSkinnable(); 1304 1305 final double paddingTop = snappedTopInset(); 1306 final double paddingRight = snappedRightInset(); 1307 final double paddingBottom = snappedBottomInset(); 1308 final double paddingLeft = snappedLeftInset(); 1309 final double w = getWidth() - (paddingLeft + paddingRight); 1310 final double h = getHeight() - (paddingTop + paddingBottom); 1311 1312 final double prefLabelWidth = snapSize(label.prefWidth(-1)); 1313 final double prefLabelHeight = snapSize(label.prefHeight(-1)); 1314 1315 final double closeBtnWidth = showCloseButton() ? snapSize(closeBtn.prefWidth(-1)) : 0; 1316 final double closeBtnHeight = showCloseButton() ? snapSize(closeBtn.prefHeight(-1)) : 0; 1317 final double minWidth = snapSize(skinnable.getTabMinWidth()); 1318 final double maxWidth = snapSize(skinnable.getTabMaxWidth()); 1319 final double maxHeight = snapSize(skinnable.getTabMaxHeight()); 1320 1321 double labelAreaWidth = prefLabelWidth; 1322 double labelWidth = prefLabelWidth; 1323 double labelHeight = prefLabelHeight; 1324 1325 final double childrenWidth = labelAreaWidth + closeBtnWidth; 1326 final double childrenHeight = Math.max(labelHeight, closeBtnHeight); 1327 1328 if (childrenWidth > maxWidth && maxWidth != Double.MAX_VALUE) { 1329 labelAreaWidth = maxWidth - closeBtnWidth; 1330 labelWidth = maxWidth - closeBtnWidth; 1331 } else if (childrenWidth < minWidth) { 1332 labelAreaWidth = minWidth - closeBtnWidth; 1333 } 1334 1335 if (childrenHeight > maxHeight && maxHeight != Double.MAX_VALUE) { 1336 labelHeight = maxHeight; 1337 } 1338 1339 if (animationState != TabAnimationState.NONE) { 1340 // if (prefWidth.getValue() < labelAreaWidth) { 1341 // labelAreaWidth = prefWidth.getValue(); 1342 // } 1343 labelAreaWidth *= animationTransition.get(); 1344 closeBtn.setVisible(false); 1345 } else { 1346 closeBtn.setVisible(showCloseButton()); 1347 } 1348 1349 1350 label.resize(labelWidth, labelHeight); 1351 1352 1353 double labelStartX = paddingLeft; 1354 1355 // If maxWidth is less than Double.MAX_VALUE, the user has 1356 // clamped the max width, but we should 1357 // position the close button at the end of the tab, 1358 // which may not necessarily be the entire width of the 1359 // provided max width. 1360 double closeBtnStartX = (maxWidth < Double.MAX_VALUE ? Math.min(w, maxWidth) : w) - paddingRight - closeBtnWidth; 1361 1362 positionInArea(label, labelStartX, paddingTop, labelAreaWidth, h, 1363 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1364 1365 if (closeBtn.isVisible()) { 1366 closeBtn.resize(closeBtnWidth, closeBtnHeight); 1367 positionInArea(closeBtn, closeBtnStartX, paddingTop, closeBtnWidth, h, 1368 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1369 } 1370 1371 // Magic numbers regretfully introduced for RT-28944 (so that 1372 // the focus rect appears as expected on Windows and Mac). 1373 // In short we use the vPadding to shift the focus rect down 1374 // into the content area (whereas previously it was being clipped 1375 // on Windows, whilst it still looked fine on Mac). In the 1376 // future we may want to improve this code to remove the 1377 // magic number. Similarly, the hPadding differs on Mac. 1378 final int vPadding = Utils.isMac() ? 2 : 3; 1379 final int hPadding = Utils.isMac() ? 2 : 1; 1380 focusIndicator.resizeRelocate( 1381 paddingLeft - hPadding, 1382 paddingTop + vPadding, 1383 w + 2 * hPadding, 1384 h - 2 * vPadding); 1385 } 1386 }; 1387 inner.getStyleClass().add("tab-container"); 1388 inner.setRotate(getSkinnable().getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F); 1389 inner.getChildren().addAll(label, closeBtn, focusIndicator); 1390 1391 getChildren().addAll(inner); 1392 1393 tooltip = tab.getTooltip(); 1394 if (tooltip != null) { 1395 Tooltip.install(this, tooltip); 1396 oldTooltip = tooltip; 1397 } 1398 1399 listener.registerChangeListener(tab.closableProperty(), e -> { 1400 inner.requestLayout(); 1401 requestLayout(); 1402 }); 1403 listener.registerChangeListener(tab.selectedProperty(), e -> { 1404 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected()); 1405 // Need to request a layout pass for inner because if the width 1406 // and height didn't not change the label or close button may have 1407 // changed. 1408 inner.requestLayout(); 1409 requestLayout(); 1410 }); 1411 listener.registerChangeListener(tab.textProperty(),e -> label.setText(getTab().getText())); 1412 listener.registerChangeListener(tab.graphicProperty(), e -> label.setGraphic(getTab().getGraphic())); 1413 listener.registerChangeListener(tab.tooltipProperty(), e -> { 1414 // uninstall the old tooltip 1415 if (oldTooltip != null) { 1416 Tooltip.uninstall(this, oldTooltip); 1417 } 1418 tooltip = tab.getTooltip(); 1419 if (tooltip != null) { 1420 // install new tooltip and save as old tooltip. 1421 Tooltip.install(this, tooltip); 1422 oldTooltip = tooltip; 1423 } 1424 }); 1425 listener.registerChangeListener(tab.disabledProperty(), e -> { 1426 updateTabDisabledState(); 1427 }); 1428 listener.registerChangeListener(tab.getTabPane().disabledProperty(), e -> { 1429 updateTabDisabledState(); 1430 }); 1431 listener.registerChangeListener(tab.styleProperty(), e -> setStyle(tab.getStyle())); 1432 1433 tab.getStyleClass().addListener(weakStyleClassListener); 1434 1435 listener.registerChangeListener(getSkinnable().tabClosingPolicyProperty(),e -> { 1436 inner.requestLayout(); 1437 requestLayout(); 1438 }); 1439 listener.registerChangeListener(getSkinnable().sideProperty(),e -> { 1440 final Side side = getSkinnable().getSide(); 1441 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP)); 1442 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT)); 1443 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM)); 1444 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT)); 1445 inner.setRotate(side == Side.BOTTOM ? 180.0F : 0.0F); 1446 if (getSkinnable().isRotateGraphic()) { 1447 updateGraphicRotation(); 1448 } 1449 }); 1450 listener.registerChangeListener(getSkinnable().rotateGraphicProperty(), e -> updateGraphicRotation()); 1451 listener.registerChangeListener(getSkinnable().tabMinWidthProperty(), e -> { 1452 requestLayout(); 1453 getSkinnable().requestLayout(); 1454 }); 1455 listener.registerChangeListener(getSkinnable().tabMaxWidthProperty(), e -> { 1456 requestLayout(); 1457 getSkinnable().requestLayout(); 1458 }); 1459 listener.registerChangeListener(getSkinnable().tabMinHeightProperty(), e -> { 1460 requestLayout(); 1461 getSkinnable().requestLayout(); 1462 }); 1463 listener.registerChangeListener(getSkinnable().tabMaxHeightProperty(), e -> { 1464 requestLayout(); 1465 getSkinnable().requestLayout(); 1466 }); 1467 1468 getProperties().put(Tab.class, tab); 1469 getProperties().put(ContextMenu.class, tab.getContextMenu()); 1470 1471 setOnContextMenuRequested((ContextMenuEvent me) -> { 1472 if (getTab().getContextMenu() != null) { 1473 getTab().getContextMenu().show(inner, me.getScreenX(), me.getScreenY()); 1474 me.consume(); 1475 } 1476 }); 1477 setOnMousePressed(new EventHandler<MouseEvent>() { 1478 @Override public void handle(MouseEvent me) { 1479 if (getTab().isDisable()) { 1480 return; 1481 } 1482 if (me.getButton().equals(MouseButton.MIDDLE)) { 1483 if (showCloseButton()) { 1484 Tab tab = getTab(); 1485 if (behavior.canCloseTab(tab)) { 1486 removeListeners(tab); 1487 behavior.closeTab(tab); 1488 } 1489 } 1490 } else if (me.getButton().equals(MouseButton.PRIMARY)) { 1491 behavior.selectTab(getTab()); 1492 } 1493 } 1494 }); 1495 1496 // initialize pseudo-class state 1497 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected()); 1498 pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisabled()); 1499 final Side side = getSkinnable().getSide(); 1500 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP)); 1501 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT)); 1502 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM)); 1503 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT)); 1504 } 1505 1506 private void updateTabDisabledState() { 1507 pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisabled()); 1508 inner.requestLayout(); 1509 requestLayout(); 1510 } 1511 1512 private void updateGraphicRotation() { 1513 if (label.getGraphic() != null) { 1514 label.getGraphic().setRotate(getSkinnable().isRotateGraphic() ? 0.0F : 1515 (getSkinnable().getSide().equals(Side.RIGHT) ? -90.0F : 1516 (getSkinnable().getSide().equals(Side.LEFT) ? 90.0F : 0.0F))); 1517 } 1518 } 1519 1520 private boolean showCloseButton() { 1521 return tab.isClosable() && 1522 (getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.ALL_TABS) || 1523 getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.SELECTED_TAB) && tab.isSelected()); 1524 } 1525 1526 private final DoubleProperty animationTransition = new SimpleDoubleProperty(this, "animationTransition", 1.0) { 1527 @Override protected void invalidated() { 1528 requestLayout(); 1529 } 1530 }; 1531 1532 private void removeListeners(Tab tab) { 1533 listener.dispose(); 1534 inner.getChildren().clear(); 1535 getChildren().clear(); 1536 setOnContextMenuRequested(null); 1537 setOnMousePressed(null); 1538 } 1539 1540 private TabAnimationState animationState = TabAnimationState.NONE; 1541 private Timeline currentAnimation; 1542 1543 @Override protected double computePrefWidth(double height) { 1544 // if (animating) { 1545 // return prefWidth.getValue(); 1546 // } 1547 double minWidth = snapSize(getSkinnable().getTabMinWidth()); 1548 double maxWidth = snapSize(getSkinnable().getTabMaxWidth()); 1549 double paddingRight = snappedRightInset(); 1550 double paddingLeft = snappedLeftInset(); 1551 double tmpPrefWidth = snapSize(label.prefWidth(-1)); 1552 1553 // only include the close button width if it is relevant 1554 if (showCloseButton()) { 1555 tmpPrefWidth += snapSize(closeBtn.prefWidth(-1)); 1556 } 1557 1558 if (tmpPrefWidth > maxWidth) { 1559 tmpPrefWidth = maxWidth; 1560 } else if (tmpPrefWidth < minWidth) { 1561 tmpPrefWidth = minWidth; 1562 } 1563 tmpPrefWidth += paddingRight + paddingLeft; 1564 // prefWidth.setValue(tmpPrefWidth); 1565 return tmpPrefWidth; 1566 } 1567 1568 @Override protected double computePrefHeight(double width) { 1569 double minHeight = snapSize(getSkinnable().getTabMinHeight()); 1570 double maxHeight = snapSize(getSkinnable().getTabMaxHeight()); 1571 double paddingTop = snappedTopInset(); 1572 double paddingBottom = snappedBottomInset(); 1573 double tmpPrefHeight = snapSize(label.prefHeight(width)); 1574 1575 if (tmpPrefHeight > maxHeight) { 1576 tmpPrefHeight = maxHeight; 1577 } else if (tmpPrefHeight < minHeight) { 1578 tmpPrefHeight = minHeight; 1579 } 1580 tmpPrefHeight += paddingTop + paddingBottom; 1581 return tmpPrefHeight; 1582 } 1583 1584 @Override protected void layoutChildren() { 1585 double w = (snapSize(getWidth()) - snappedRightInset() - snappedLeftInset()) * animationTransition.getValue(); 1586 inner.resize(w, snapSize(getHeight()) - snappedTopInset() - snappedBottomInset()); 1587 inner.relocate(snappedLeftInset(), snappedTopInset()); 1588 } 1589 1590 @Override protected void setWidth(double value) { 1591 super.setWidth(value); 1592 clip.setWidth(value); 1593 } 1594 1595 @Override protected void setHeight(double value) { 1596 super.setHeight(value); 1597 clip.setHeight(value); 1598 } 1599 1600 /** {@inheritDoc} */ 1601 @Override 1602 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1603 switch (attribute) { 1604 case TEXT: return getTab().getText(); 1605 case SELECTED: return selectedTab == getTab(); 1606 default: return super.queryAccessibleAttribute(attribute, parameters); 1607 } 1608 } 1609 1610 /** {@inheritDoc} */ 1611 @Override 1612 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 1613 switch (action) { 1614 case REQUEST_FOCUS: 1615 getSkinnable().getSelectionModel().select(getTab()); 1616 break; 1617 default: super.executeAccessibleAction(action, parameters); 1618 } 1619 } 1620 1621 } /* End TabHeaderSkin */ 1622 1623 private static final PseudoClass SELECTED_PSEUDOCLASS_STATE = 1624 PseudoClass.getPseudoClass("selected"); 1625 private static final PseudoClass TOP_PSEUDOCLASS_STATE = 1626 PseudoClass.getPseudoClass("top"); 1627 private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE = 1628 PseudoClass.getPseudoClass("bottom"); 1629 private static final PseudoClass LEFT_PSEUDOCLASS_STATE = 1630 PseudoClass.getPseudoClass("left"); 1631 private static final PseudoClass RIGHT_PSEUDOCLASS_STATE = 1632 PseudoClass.getPseudoClass("right"); 1633 private static final PseudoClass DISABLED_PSEUDOCLASS_STATE = 1634 PseudoClass.getPseudoClass("disabled"); 1635 1636 1637 /************************************************************************** 1638 * 1639 * TabContentRegion: each tab has one to contain the tab's content node 1640 * 1641 **************************************************************************/ 1642 static class TabContentRegion extends StackPane { 1643 1644 private Tab tab; 1645 1646 private InvalidationListener tabContentListener = valueModel -> { 1647 updateContent(); 1648 }; 1649 private InvalidationListener tabSelectedListener = new InvalidationListener() { 1650 @Override public void invalidated(Observable valueModel) { 1651 setVisible(tab.isSelected()); 1652 } 1653 }; 1654 1655 private WeakInvalidationListener weakTabContentListener = 1656 new WeakInvalidationListener(tabContentListener); 1657 private WeakInvalidationListener weakTabSelectedListener = 1658 new WeakInvalidationListener(tabSelectedListener); 1659 1660 public Tab getTab() { 1661 return tab; 1662 } 1663 1664 public TabContentRegion(Tab tab) { 1665 getStyleClass().setAll("tab-content-area"); 1666 setManaged(false); 1667 this.tab = tab; 1668 updateContent(); 1669 setVisible(tab.isSelected()); 1670 1671 tab.selectedProperty().addListener(weakTabSelectedListener); 1672 tab.contentProperty().addListener(weakTabContentListener); 1673 } 1674 1675 private void updateContent() { 1676 Node newContent = getTab().getContent(); 1677 if (newContent == null) { 1678 getChildren().clear(); 1679 } else { 1680 getChildren().setAll(newContent); 1681 } 1682 } 1683 1684 private void removeListeners(Tab tab) { 1685 tab.selectedProperty().removeListener(weakTabSelectedListener); 1686 tab.contentProperty().removeListener(weakTabContentListener); 1687 } 1688 1689 } /* End TabContentRegion */ 1690 1691 /************************************************************************** 1692 * 1693 * TabControlButtons: controls to manipulate tab interaction 1694 * 1695 **************************************************************************/ 1696 class TabControlButtons extends StackPane { 1697 private StackPane inner; 1698 private StackPane downArrow; 1699 private Pane downArrowBtn; 1700 private boolean showControlButtons; 1701 private ContextMenu popup; 1702 1703 public TabControlButtons() { 1704 getStyleClass().setAll("control-buttons-tab"); 1705 1706 TabPane tabPane = getSkinnable(); 1707 1708 downArrowBtn = new Pane(); 1709 downArrowBtn.getStyleClass().setAll("tab-down-button"); 1710 downArrowBtn.setVisible(isShowTabsMenu()); 1711 downArrow = new StackPane(); 1712 downArrow.setManaged(false); 1713 downArrow.getStyleClass().setAll("arrow"); 1714 downArrow.setRotate(tabPane.getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F); 1715 downArrowBtn.getChildren().add(downArrow); 1716 downArrowBtn.setOnMouseClicked(me -> { 1717 showPopupMenu(); 1718 }); 1719 1720 setupPopupMenu(); 1721 1722 inner = new StackPane() { 1723 @Override protected double computePrefWidth(double height) { 1724 double pw; 1725 double maxArrowWidth = ! isShowTabsMenu() ? 0 : snapSize(downArrow.prefWidth(getHeight())) + snapSize(downArrowBtn.prefWidth(getHeight())); 1726 pw = 0.0F; 1727 if (isShowTabsMenu()) { 1728 pw += maxArrowWidth; 1729 } 1730 if (pw > 0) { 1731 pw += snappedLeftInset() + snappedRightInset(); 1732 } 1733 return pw; 1734 } 1735 1736 @Override protected double computePrefHeight(double width) { 1737 double height = 0.0F; 1738 if (isShowTabsMenu()) { 1739 height = Math.max(height, snapSize(downArrowBtn.prefHeight(width))); 1740 } 1741 if (height > 0) { 1742 height += snappedTopInset() + snappedBottomInset(); 1743 } 1744 return height; 1745 } 1746 1747 @Override protected void layoutChildren() { 1748 if (isShowTabsMenu()) { 1749 double x = 0; 1750 double y = snappedTopInset(); 1751 double w = snapSize(getWidth()) - x + snappedLeftInset(); 1752 double h = snapSize(getHeight()) - y + snappedBottomInset(); 1753 positionArrow(downArrowBtn, downArrow, x, y, w, h); 1754 } 1755 } 1756 1757 private void positionArrow(Pane btn, StackPane arrow, double x, double y, double width, double height) { 1758 btn.resize(width, height); 1759 positionInArea(btn, x, y, width, height, /*baseline ignored*/0, 1760 HPos.CENTER, VPos.CENTER); 1761 // center arrow region within arrow button 1762 double arrowWidth = snapSize(arrow.prefWidth(-1)); 1763 double arrowHeight = snapSize(arrow.prefHeight(-1)); 1764 arrow.resize(arrowWidth, arrowHeight); 1765 positionInArea(arrow, btn.snappedLeftInset(), btn.snappedTopInset(), 1766 width - btn.snappedLeftInset() - btn.snappedRightInset(), 1767 height - btn.snappedTopInset() - btn.snappedBottomInset(), 1768 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1769 } 1770 }; 1771 inner.getStyleClass().add("container"); 1772 inner.getChildren().add(downArrowBtn); 1773 1774 getChildren().add(inner); 1775 1776 tabPane.sideProperty().addListener(valueModel -> { 1777 Side tabPosition = getSkinnable().getSide(); 1778 downArrow.setRotate(tabPosition.equals(Side.BOTTOM)? 180.0F : 0.0F); 1779 }); 1780 tabPane.getTabs().addListener((ListChangeListener<Tab>) c -> setupPopupMenu()); 1781 showControlButtons = false; 1782 if (isShowTabsMenu()) { 1783 showControlButtons = true; 1784 requestLayout(); 1785 } 1786 getProperties().put(ContextMenu.class, popup); 1787 } 1788 1789 private boolean showTabsMenu = false; 1790 1791 private void showTabsMenu(boolean value) { 1792 final boolean wasTabsMenuShowing = isShowTabsMenu(); 1793 this.showTabsMenu = value; 1794 1795 if (showTabsMenu && !wasTabsMenuShowing) { 1796 downArrowBtn.setVisible(true); 1797 showControlButtons = true; 1798 inner.requestLayout(); 1799 tabHeaderArea.requestLayout(); 1800 } else if (!showTabsMenu && wasTabsMenuShowing) { 1801 hideControlButtons(); 1802 } 1803 } 1804 1805 private boolean isShowTabsMenu() { 1806 return showTabsMenu; 1807 } 1808 1809 @Override protected double computePrefWidth(double height) { 1810 double pw = snapSize(inner.prefWidth(height)); 1811 if (pw > 0) { 1812 pw += snappedLeftInset() + snappedRightInset(); 1813 } 1814 return pw; 1815 } 1816 1817 @Override protected double computePrefHeight(double width) { 1818 return Math.max(getSkinnable().getTabMinHeight(), snapSize(inner.prefHeight(width))) + 1819 snappedTopInset() + snappedBottomInset(); 1820 } 1821 1822 @Override protected void layoutChildren() { 1823 double x = snappedLeftInset(); 1824 double y = snappedTopInset(); 1825 double w = snapSize(getWidth()) - x + snappedRightInset(); 1826 double h = snapSize(getHeight()) - y + snappedBottomInset(); 1827 1828 if (showControlButtons) { 1829 showControlButtons(); 1830 showControlButtons = false; 1831 } 1832 1833 inner.resize(w, h); 1834 positionInArea(inner, x, y, w, h, /*baseline ignored*/0, HPos.CENTER, VPos.BOTTOM); 1835 } 1836 1837 private void showControlButtons() { 1838 setVisible(true); 1839 if (popup == null) { 1840 setupPopupMenu(); 1841 } 1842 } 1843 1844 private void hideControlButtons() { 1845 // If the scroll arrows or tab menu is still visible we don't want 1846 // to hide it animate it back it. 1847 if (isShowTabsMenu()) { 1848 showControlButtons = true; 1849 } else { 1850 setVisible(false); 1851 popup.getItems().clear(); 1852 popup = null; 1853 } 1854 1855 // This needs to be called when we are in the left tabPosition 1856 // to allow for the clip offset to move properly (otherwise 1857 // it jumps too early - before the animation is done). 1858 requestLayout(); 1859 } 1860 1861 private void setupPopupMenu() { 1862 if (popup == null) { 1863 popup = new ContextMenu(); 1864 } 1865 popup.getItems().clear(); 1866 ToggleGroup group = new ToggleGroup(); 1867 ObservableList<RadioMenuItem> menuitems = FXCollections.<RadioMenuItem>observableArrayList(); 1868 for (final Tab tab : getSkinnable().getTabs()) { 1869 TabMenuItem item = new TabMenuItem(tab); 1870 item.setToggleGroup(group); 1871 item.setOnAction(t -> getSkinnable().getSelectionModel().select(tab)); 1872 menuitems.add(item); 1873 } 1874 popup.getItems().addAll(menuitems); 1875 } 1876 1877 private void showPopupMenu() { 1878 for (MenuItem mi: popup.getItems()) { 1879 TabMenuItem tmi = (TabMenuItem)mi; 1880 if (selectedTab.equals(tmi.getTab())) { 1881 tmi.setSelected(true); 1882 break; 1883 } 1884 } 1885 popup.show(downArrowBtn, Side.BOTTOM, 0, 0); 1886 } 1887 } /* End TabControlButtons*/ 1888 1889 static class TabMenuItem extends RadioMenuItem { 1890 Tab tab; 1891 1892 private InvalidationListener disableListener = new InvalidationListener() { 1893 @Override public void invalidated(Observable o) { 1894 setDisable(tab.isDisable()); 1895 } 1896 }; 1897 1898 private WeakInvalidationListener weakDisableListener = 1899 new WeakInvalidationListener(disableListener); 1900 1901 public TabMenuItem(final Tab tab) { 1902 super(tab.getText(), TabPaneSkin.clone(tab.getGraphic())); 1903 this.tab = tab; 1904 setDisable(tab.isDisable()); 1905 tab.disableProperty().addListener(weakDisableListener); 1906 textProperty().bind(tab.textProperty()); 1907 } 1908 1909 public Tab getTab() { 1910 return tab; 1911 } 1912 1913 public void dispose() { 1914 tab.disableProperty().removeListener(weakDisableListener); 1915 } 1916 } 1917 1918 @Override 1919 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1920 switch (attribute) { 1921 case FOCUS_ITEM: return tabHeaderArea.getTabHeaderSkin(selectedTab); 1922 case ITEM_COUNT: return tabHeaderArea.headersRegion.getChildren().size(); 1923 case ITEM_AT_INDEX: { 1924 Integer index = (Integer)parameters[0]; 1925 if (index == null) return null; 1926 return tabHeaderArea.headersRegion.getChildren().get(index); 1927 } 1928 default: return super.queryAccessibleAttribute(attribute, parameters); 1929 } 1930 } 1931 1932 // -------------------------- 1933 // Tab Reordering 1934 // -------------------------- 1935 private enum DragState { 1936 NONE, 1937 START, 1938 REORDER 1939 } 1940 private EventHandler<MouseEvent> headerDraggedHandler = this::handleHeaderDragged; 1941 private EventHandler<MouseEvent> headerMousePressedHandler = this::handleHeaderMousePressed; 1942 private EventHandler<MouseEvent> headerMouseReleasedHandler = this::handleHeaderMouseReleased; 1943 1944 private int dragTabHeaderIndex; 1945 private TabHeaderSkin dragTabHeader; 1946 private TabHeaderSkin dropTabHeader; 1947 private StackPane headersRegion; 1948 private DragState dragState; 1949 private int xLayoutDirection; 1950 private Point2D dragEventStartLoc; 1951 private Point2D dragEventPrevLoc; 1952 private final static int Drag_LTR = 1; 1953 private final static int Drag_RTL = -1; 1954 private int prevDragDirection = Drag_LTR; 1955 private final double DRAG_DIST_THRESHOLD = 0.75; 1956 1957 // Reordering Animation 1958 private static double ANIM_DURATION = 120; 1959 private TabHeaderSkin dropAnimHeader; 1960 private Tab swapTab = null; 1961 private double dropHeaderSourceX; 1962 private double dropHeaderTransitionX; 1963 private final Animation dropHeaderAnim = new Transition() { 1964 { 1965 setInterpolator(Interpolator.EASE_BOTH); 1966 setCycleDuration(Duration.millis(ANIM_DURATION)); 1967 setOnFinished(event -> { 1968 completeHeaderReordering(); 1969 }); 1970 } 1971 protected void interpolate(double frac) { 1972 dropAnimHeader.setLayoutX(dropHeaderSourceX + dropHeaderTransitionX * frac); 1973 } 1974 }; 1975 private double dragHeaderStartX; 1976 private double dragHeaderDestX; 1977 private double dragHeaderSourceX; 1978 private double dragHeaderTransitionX; 1979 private final Animation dragHeaderAnim = new Transition() { 1980 { 1981 setInterpolator(Interpolator.EASE_OUT); 1982 setCycleDuration(Duration.millis(ANIM_DURATION)); 1983 setOnFinished(event -> { 1984 resetDrag(); 1985 }); 1986 } 1987 protected void interpolate(double frac) { 1988 dragTabHeader.setLayoutX(dragHeaderSourceX + dragHeaderTransitionX * frac); 1989 } 1990 }; 1991 1992 // Helper methods for managing the listeners based on TabDragPolicy. 1993 private void addReorderListeners(Node n) { 1994 n.addEventHandler(MouseEvent.MOUSE_PRESSED, headerMousePressedHandler); 1995 n.addEventHandler(MouseEvent.MOUSE_RELEASED, headerMouseReleasedHandler); 1996 n.addEventHandler(MouseEvent.MOUSE_DRAGGED, headerDraggedHandler); 1997 } 1998 1999 private void removeReorderListeners(Node n) { 2000 n.removeEventHandler(MouseEvent.MOUSE_PRESSED, headerMousePressedHandler); 2001 n.removeEventHandler(MouseEvent.MOUSE_RELEASED, headerMouseReleasedHandler); 2002 n.removeEventHandler(MouseEvent.MOUSE_DRAGGED, headerDraggedHandler); 2003 } 2004 2005 private ListChangeListener childListener = new ListChangeListener<Node>() { 2006 public void onChanged(Change<? extends Node> change) { 2007 while (change.next()) { 2008 if (change.wasAdded()) { 2009 for(Node n : change.getAddedSubList()) { 2010 addReorderListeners(n); 2011 } 2012 } 2013 if (change.wasRemoved()) { 2014 for(Node n : change.getRemoved()) { 2015 removeReorderListeners(n); 2016 } 2017 } 2018 } 2019 } 2020 }; 2021 2022 private void updateListeners() { 2023 if (getSkinnable().getTabDragPolicy() == TabDragPolicy.FIXED || 2024 getSkinnable().getTabDragPolicy() == null) { 2025 for (Node n : headersRegion.getChildren()) { 2026 removeReorderListeners(n); 2027 } 2028 headersRegion.getChildren().removeListener(childListener); 2029 } else if (getSkinnable().getTabDragPolicy() == TabDragPolicy.REORDER) { 2030 for (Node n : headersRegion.getChildren()) { 2031 addReorderListeners(n); 2032 } 2033 headersRegion.getChildren().addListener(childListener); 2034 } 2035 } 2036 2037 private void setupReordering(StackPane headerRegion) { 2038 dragState = DragState.NONE; 2039 headersRegion = headerRegion; 2040 updateListeners(); 2041 getSkinnable().tabDragPolicyProperty().addListener((observable, oldValue, newValue) -> { 2042 if (oldValue != newValue) { 2043 updateListeners(); 2044 } 2045 }); 2046 } 2047 2048 private void handleHeaderMousePressed(MouseEvent event) { 2049 startDrag(event); 2050 } 2051 2052 private void handleHeaderMouseReleased(MouseEvent event) { 2053 stopDrag(); 2054 event.consume(); 2055 } 2056 2057 private void handleHeaderDragged(MouseEvent event) { 2058 perfromDrag(event); 2059 } 2060 2061 private Point2D rotate(Point2D pt) { 2062 double angle = getSkinnable().getRotate(); 2063 if (angle == 0) { 2064 return pt; 2065 } 2066 // Rotate the point pt by -(rotation angle of TabPane) with 2067 // respect to 0,0,1 axis passing through dragEventStartLoc, 2068 // and return the rotated point. 2069 double x1 = pt.getX() - dragEventStartLoc.getX(); 2070 double y1 = pt.getY() - dragEventStartLoc.getY(); 2071 double x = x1 * Math.cos(Math.toRadians(angle)) + y1 * Math.sin(Math.toRadians(angle)); 2072 double y = y1 * Math.cos(Math.toRadians(angle)) - x1 * Math.sin(Math.toRadians(angle)); 2073 x += dragEventStartLoc.getX(); 2074 y += dragEventStartLoc.getY(); 2075 return new Point2D(x , y); 2076 } 2077 2078 private double getDragDelta(Point2D curr, Point2D prev) { 2079 if (getSkinnable().getSide().equals(Side.LEFT) || 2080 getSkinnable().getSide().equals(Side.RIGHT)) { 2081 return curr.getY() - prev.getY(); 2082 } 2083 return curr.getX() - prev.getX(); 2084 } 2085 2086 private int deriveTabHeaderLayoutXDirection() { 2087 if (getSkinnable().getSide().equals(Side.TOP) || 2088 getSkinnable().getSide().equals(Side.RIGHT)) { 2089 // TabHeaderSkin are laid out in left to right direction 2090 return Drag_LTR; 2091 } 2092 // TabHeaderSkin are laid out in right to left direction 2093 return Drag_RTL; 2094 } 2095 2096 private void perfromDrag(MouseEvent event) { 2097 int dragDirection; 2098 double dragHeaderNewLayoutX; 2099 Bounds dragHeaderBounds; 2100 Bounds dropHeaderBounds; 2101 double draggedDist; 2102 Point2D mouseCurrentLoc = rotate(new Point2D(event.getScreenX(), event.getScreenY())); 2103 double dragDelta = getDragDelta(mouseCurrentLoc, dragEventPrevLoc); 2104 if (getSkinnable().getNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) { 2105 if (getSkinnable().getSide().equals(Side.TOP) || 2106 getSkinnable().getSide().equals(Side.BOTTOM)) 2107 dragDelta = -dragDelta; 2108 } 2109 2110 // Stop dropHeaderAnim if direction of drag is changed 2111 if (dragDelta > 0) { 2112 dragDirection = Drag_LTR; 2113 } else { 2114 dragDirection = Drag_RTL; 2115 } 2116 if (prevDragDirection != dragDirection) { 2117 stopAnim(dropHeaderAnim); 2118 prevDragDirection = dragDirection; 2119 } 2120 2121 dragHeaderNewLayoutX = dragTabHeader.getLayoutX() + xLayoutDirection * dragDelta; 2122 2123 if (dragHeaderNewLayoutX >= 0 && 2124 dragHeaderNewLayoutX + dragTabHeader.getWidth() <= headersRegion.getWidth()) { 2125 2126 dragState = DragState.REORDER; 2127 dragTabHeader.setLayoutX(dragHeaderNewLayoutX); 2128 dragHeaderBounds = dragTabHeader.getBoundsInParent(); 2129 2130 if (dragDirection == Drag_LTR) { 2131 // Dragging the tab header towards right 2132 // Last tab header can not be dragged towards right. 2133 // When the mouse is moved too fast, sufficient number of events 2134 // are not generated. Hence it is required to check all possible 2135 // headers to be reordered. 2136 for (int i = dragTabHeaderIndex + 1; i < headersRegion.getChildren().size(); i++) { 2137 dropTabHeader = (TabHeaderSkin) headersRegion.getChildren().get(i); 2138 2139 // Check if the tab header is already reordering. 2140 if (dropAnimHeader != dropTabHeader) { 2141 dropHeaderBounds = dropTabHeader.getBoundsInParent(); 2142 2143 if (xLayoutDirection == Drag_LTR) { 2144 draggedDist = dragHeaderBounds.getMaxX() - dropHeaderBounds.getMinX(); 2145 } else { 2146 draggedDist = dropHeaderBounds.getMaxX() - dragHeaderBounds.getMinX(); 2147 } 2148 2149 // A tab is reordered when dragged tab corsses DRAG_DIST_THRESHOLD% of next tabs width. 2150 if (draggedDist > dropHeaderBounds.getWidth() * DRAG_DIST_THRESHOLD) { 2151 stopAnim(dropHeaderAnim); 2152 // Distance by which tab header should be animated in X. 2153 dropHeaderTransitionX = xLayoutDirection * -dragHeaderBounds.getWidth(); 2154 if (xLayoutDirection == Drag_LTR) { 2155 dragHeaderDestX = dropHeaderBounds.getMaxX() - dragHeaderBounds.getWidth(); 2156 } else { 2157 dragHeaderDestX = dropHeaderBounds.getMinX(); 2158 } 2159 startHeaderReorderingAnim(); 2160 } else { 2161 break; 2162 } 2163 } 2164 } 2165 } else { 2166 // Dragging the tab header towards left 2167 // First tab header can not be dragged towards left. 2168 // When the mouse is moved too fast, sufficient number of events 2169 // are not generated. Hence it is required to check all possible 2170 // headers to be reordered. 2171 for (int i = dragTabHeaderIndex - 1; i >= 0; i--) { 2172 dropTabHeader = (TabHeaderSkin) headersRegion.getChildren().get(i); 2173 2174 // Check if the tab header is already reordering. 2175 if (dropAnimHeader != dropTabHeader) { 2176 dropHeaderBounds = dropTabHeader.getBoundsInParent(); 2177 2178 if (xLayoutDirection == Drag_LTR) { 2179 draggedDist = dropHeaderBounds.getMaxX() - dragHeaderBounds.getMinX(); 2180 } else { 2181 draggedDist = dragHeaderBounds.getMaxX() - dropHeaderBounds.getMinX(); 2182 } 2183 2184 // A tab is reordered when dragged tab crosses DRAG_DIST_THRESHOLD% of next tabs width. 2185 if (draggedDist > dropHeaderBounds.getWidth() * DRAG_DIST_THRESHOLD) { 2186 stopAnim(dropHeaderAnim); 2187 // Distance by which tab header should be animated in X position. 2188 dropHeaderTransitionX = xLayoutDirection * dragHeaderBounds.getWidth(); 2189 if (xLayoutDirection == Drag_LTR) { 2190 dragHeaderDestX = dropHeaderBounds.getMinX(); 2191 } else { 2192 dragHeaderDestX = dropHeaderBounds.getMaxX() - dragHeaderBounds.getWidth(); 2193 } 2194 startHeaderReorderingAnim(); 2195 } else { 2196 break; 2197 } 2198 } 2199 } 2200 } 2201 } 2202 dragEventPrevLoc = mouseCurrentLoc; 2203 event.consume(); 2204 } 2205 2206 private void startDrag(MouseEvent event) { 2207 stopAnim(dropHeaderAnim); 2208 stopAnim(dragHeaderAnim); 2209 dragTabHeader = (TabHeaderSkin) event.getSource(); 2210 if (dragTabHeader != null) { 2211 dragState = DragState.START; 2212 swapTab = null; 2213 xLayoutDirection = deriveTabHeaderLayoutXDirection(); 2214 dragEventStartLoc = new Point2D(event.getScreenX(), event.getScreenY()); 2215 dragEventPrevLoc = new Point2D(event.getScreenX(), event.getScreenY()); 2216 dragTabHeaderIndex = headersRegion.getChildren().indexOf(dragTabHeader); 2217 dragTabHeader.setViewOrder(0); 2218 dragHeaderStartX = dragHeaderDestX = dragTabHeader.getLayoutX(); 2219 } 2220 } 2221 2222 private void stopDrag() { 2223 if (dragState == DragState.START) { 2224 // No drag action was performed. 2225 resetDrag(); 2226 return; 2227 } 2228 // Animate tab header being dragged to its final position. 2229 dragHeaderSourceX = dragTabHeader.getLayoutX(); 2230 dragHeaderTransitionX = dragHeaderDestX - dragHeaderSourceX; 2231 dragHeaderAnim.playFromStart(); 2232 2233 // Reorder the tab list 2234 if (dragHeaderStartX != dragHeaderDestX) { 2235 ((TabObservableList<Tab>) getSkinnable().getTabs()).reorder(dragTabHeader.tab, swapTab); 2236 swapTab = null; 2237 } 2238 } 2239 2240 private void resetDrag() { 2241 dragState = DragState.NONE; 2242 dragTabHeader.setViewOrder(1); 2243 dragTabHeader = null; 2244 dropTabHeader = null; 2245 headersRegion.requestLayout(); 2246 } 2247 2248 // Animate tab header being dropped-on to its new position. 2249 private void startHeaderReorderingAnim() { 2250 dropAnimHeader = dropTabHeader; 2251 swapTab = dropAnimHeader.tab; 2252 dropHeaderSourceX = dropAnimHeader.getLayoutX(); 2253 dropHeaderAnim.playFromStart(); 2254 } 2255 2256 // Remove dropAnimHeader and add at the index position of dragTabHeader. 2257 private void completeHeaderReordering() { 2258 if (dropAnimHeader != null) { 2259 headersRegion.getChildren().remove(dropAnimHeader); 2260 headersRegion.getChildren().add(dragTabHeaderIndex, dropAnimHeader); 2261 dropAnimHeader = null; 2262 headersRegion.requestLayout(); 2263 dragTabHeaderIndex = headersRegion.getChildren().indexOf(dragTabHeader); 2264 } 2265 } 2266 2267 // Helper method to stop an animation. 2268 private void stopAnim(Animation anim) { 2269 if (anim.getStatus() == Animation.Status.RUNNING) { 2270 anim.getOnFinished().handle(null); 2271 anim.stop(); 2272 } 2273 } 2274 }