1 /* 2 * Copyright (c) 2011, 2018, 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 (isSelectingTab) { 830 ensureSelectedTabIsVisible(); 831 } else { 832 validateScrollOffset(); 833 } 834 } 835 isSelectingTab = false; 836 837 Side tabPosition = getSkinnable().getSide(); 838 double tabBackgroundHeight = snapSize(prefHeight(-1)); 839 double tabX = (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) ? 840 snapSize(getWidth()) - getScrollOffset() : getScrollOffset(); 841 842 updateHeaderClip(); 843 for (Node node : getChildren()) { 844 TabHeaderSkin tabHeader = (TabHeaderSkin)node; 845 846 // size and position the header relative to the other headers 847 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1) * tabHeader.animationTransition.get()); 848 double tabHeaderPrefHeight = snapSize(tabHeader.prefHeight(-1)); 849 tabHeader.resize(tabHeaderPrefWidth, tabHeaderPrefHeight); 850 851 // This ensures that the tabs are located in the correct position 852 // when there are tabs of differing heights. 853 double startY = tabPosition.equals(Side.BOTTOM) ? 854 0 : tabBackgroundHeight - tabHeaderPrefHeight - snappedBottomInset(); 855 if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) { 856 // build from the right 857 tabX -= tabHeaderPrefWidth; 858 if (dragState != DragState.REORDER || 859 (tabHeader != dragTabHeader && tabHeader != dropAnimHeader)) { 860 tabHeader.relocate(tabX, startY); 861 } 862 } else { 863 // build from the left 864 if (dragState != DragState.REORDER || 865 (tabHeader != dragTabHeader && tabHeader != dropAnimHeader)) { 866 tabHeader.relocate(tabX, startY); 867 } 868 tabX += tabHeaderPrefWidth; 869 } 870 } 871 } 872 873 }; 874 headersRegion.getStyleClass().setAll("headers-region"); 875 headersRegion.setClip(headerClip); 876 setupReordering(headersRegion); 877 878 headerBackground = new StackPane(); 879 headerBackground.getStyleClass().setAll("tab-header-background"); 880 881 int i = 0; 882 for (Tab tab: tabPane.getTabs()) { 883 addTab(tab, i++); 884 } 885 886 controlButtons = new TabControlButtons(); 887 controlButtons.setVisible(false); 888 if (controlButtons.isVisible()) { 889 controlButtons.setVisible(true); 890 } 891 getChildren().addAll(headerBackground, headersRegion, controlButtons); 892 893 // support for mouse scroll of header area (for when the tabs exceed 894 // the available space). 895 // Scrolling the mouse wheel downwards results in the tabs scrolling left (i.e. exposing the right-most tabs) 896 // Scrolling the mouse wheel upwards results in the tabs scrolling right (i.e. exposing th left-most tabs) 897 addEventHandler(ScrollEvent.SCROLL, (ScrollEvent e) -> { 898 Side side = getSkinnable().getSide(); 899 side = side == null ? Side.TOP : side; 900 switch (side) { 901 default: 902 case TOP: 903 case BOTTOM: 904 setScrollOffset(scrollOffset + e.getDeltaY()); 905 break; 906 case LEFT: 907 case RIGHT: 908 setScrollOffset(scrollOffset - e.getDeltaY()); 909 break; 910 } 911 912 }); 913 } 914 915 private void updateHeaderClip() { 916 Side tabPosition = getSkinnable().getSide(); 917 918 double x = 0; 919 double y = 0; 920 double clipWidth = 0; 921 double clipHeight = 0; 922 double maxWidth = 0; 923 double shadowRadius = 0; 924 double clipOffset = firstTabIndent(); 925 double controlButtonPrefWidth = snapSize(controlButtons.prefWidth(-1)); 926 927 measureClosingTabs = true; 928 double headersPrefWidth = snapSize(headersRegion.prefWidth(-1)); 929 measureClosingTabs = false; 930 931 double headersPrefHeight = snapSize(headersRegion.prefHeight(-1)); 932 933 // Add the spacer if isShowTabsMenu is true. 934 if (controlButtonPrefWidth > 0) { 935 controlButtonPrefWidth = controlButtonPrefWidth + SPACER; 936 } 937 938 if (headersRegion.getEffect() instanceof DropShadow) { 939 DropShadow shadow = (DropShadow)headersRegion.getEffect(); 940 shadowRadius = shadow.getRadius(); 941 } 942 943 maxWidth = snapSize(getWidth()) - controlButtonPrefWidth - clipOffset; 944 if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) { 945 if (headersPrefWidth < maxWidth) { 946 clipWidth = headersPrefWidth + shadowRadius; 947 } else { 948 x = headersPrefWidth - maxWidth; 949 clipWidth = maxWidth + shadowRadius; 950 } 951 clipHeight = headersPrefHeight; 952 } else { 953 // If x = 0 the header region's drop shadow is clipped. 954 x = -shadowRadius; 955 clipWidth = (headersPrefWidth < maxWidth ? headersPrefWidth : maxWidth) + shadowRadius; 956 clipHeight = headersPrefHeight; 957 } 958 959 headerClip.setX(x); 960 headerClip.setY(y); 961 headerClip.setWidth(clipWidth); 962 headerClip.setHeight(clipHeight); 963 } 964 965 private void addTab(Tab tab, int addToIndex) { 966 TabHeaderSkin tabHeaderSkin = new TabHeaderSkin(tab); 967 headersRegion.getChildren().add(addToIndex, tabHeaderSkin); 968 } 969 970 private void removeTab(Tab tab) { 971 TabHeaderSkin tabHeaderSkin = getTabHeaderSkin(tab); 972 if (tabHeaderSkin != null) { 973 headersRegion.getChildren().remove(tabHeaderSkin); 974 } 975 } 976 977 private TabHeaderSkin getTabHeaderSkin(Tab tab) { 978 for (Node child: headersRegion.getChildren()) { 979 TabHeaderSkin tabHeaderSkin = (TabHeaderSkin)child; 980 if (tabHeaderSkin.getTab().equals(tab)) { 981 return tabHeaderSkin; 982 } 983 } 984 return null; 985 } 986 987 private boolean tabsFit() { 988 double headerPrefWidth = snapSize(headersRegion.prefWidth(-1)); 989 double controlTabWidth = snapSize(controlButtons.prefWidth(-1)); 990 double visibleWidth = headerPrefWidth + controlTabWidth + firstTabIndent() + SPACER; 991 return visibleWidth < getWidth(); 992 } 993 994 private void ensureSelectedTabIsVisible() { 995 // work out the visible width of the tab header 996 double tabPaneWidth = snapSize(isHorizontal() ? getSkinnable().getWidth() : getSkinnable().getHeight()); 997 double controlTabWidth = snapSize(controlButtons.getWidth()); 998 double visibleWidth = tabPaneWidth - controlTabWidth - firstTabIndent() - SPACER; 999 1000 // and get where the selected tab is in the header area 1001 double offset = 0.0; 1002 double selectedTabOffset = 0.0; 1003 double selectedTabWidth = 0.0; 1004 for (Node node : headersRegion.getChildren()) { 1005 TabHeaderSkin tabHeader = (TabHeaderSkin)node; 1006 1007 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1)); 1008 1009 if (selectedTab != null && selectedTab.equals(tabHeader.getTab())) { 1010 selectedTabOffset = offset; 1011 selectedTabWidth = tabHeaderPrefWidth; 1012 } 1013 offset += tabHeaderPrefWidth; 1014 } 1015 1016 final double scrollOffset = getScrollOffset(); 1017 final double selectedTabStartX = selectedTabOffset; 1018 final double selectedTabEndX = selectedTabOffset + selectedTabWidth; 1019 1020 final double visibleAreaEndX = visibleWidth; 1021 1022 if (selectedTabStartX < -scrollOffset) { 1023 setScrollOffset(-selectedTabStartX); 1024 } else if (selectedTabEndX > (visibleAreaEndX - scrollOffset)) { 1025 setScrollOffset(visibleAreaEndX - selectedTabEndX); 1026 } 1027 } 1028 1029 public double getScrollOffset() { 1030 return scrollOffset; 1031 } 1032 1033 private void validateScrollOffset() { 1034 setScrollOffset(getScrollOffset()); 1035 } 1036 1037 private void setScrollOffset(double newScrollOffset) { 1038 // work out the visible width of the tab header 1039 double tabPaneWidth = snapSize(isHorizontal() ? getSkinnable().getWidth() : getSkinnable().getHeight()); 1040 double controlTabWidth = snapSize(controlButtons.getWidth()); 1041 double visibleWidth = tabPaneWidth - controlTabWidth - firstTabIndent() - SPACER; 1042 1043 // measure the width of all tabs 1044 double offset = 0.0; 1045 for (Node node : headersRegion.getChildren()) { 1046 TabHeaderSkin tabHeader = (TabHeaderSkin)node; 1047 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1)); 1048 offset += tabHeaderPrefWidth; 1049 } 1050 1051 double actualNewScrollOffset; 1052 1053 if ((visibleWidth - newScrollOffset) > offset && newScrollOffset < 0) { 1054 // need to make sure the right-most tab is attached to the 1055 // right-hand side of the tab header (e.g. if the tab header area width 1056 // is expanded), and if it isn't modify the scroll offset to bring 1057 // it into line. See RT-35194 for a test case. 1058 actualNewScrollOffset = visibleWidth - offset; 1059 } else if (newScrollOffset > 0) { 1060 // need to prevent the left-most tab from becoming detached 1061 // from the left-hand side of the tab header. 1062 actualNewScrollOffset = 0; 1063 } else { 1064 actualNewScrollOffset = newScrollOffset; 1065 } 1066 1067 if (Math.abs(actualNewScrollOffset - scrollOffset) > 0.001) { 1068 scrollOffset = actualNewScrollOffset; 1069 headersRegion.requestLayout(); 1070 } 1071 } 1072 1073 private double firstTabIndent() { 1074 switch (getSkinnable().getSide()) { 1075 case TOP: 1076 case BOTTOM: 1077 return snappedLeftInset(); 1078 case RIGHT: 1079 case LEFT: 1080 return snappedTopInset(); 1081 default: 1082 return 0; 1083 } 1084 } 1085 1086 @Override protected double computePrefWidth(double height) { 1087 double padding = isHorizontal() ? 1088 snappedLeftInset() + snappedRightInset() : 1089 snappedTopInset() + snappedBottomInset(); 1090 return snapSize(headersRegion.prefWidth(height)) + controlButtons.prefWidth(height) + 1091 firstTabIndent() + SPACER + padding; 1092 } 1093 1094 @Override protected double computePrefHeight(double width) { 1095 double padding = isHorizontal() ? 1096 snappedTopInset() + snappedBottomInset() : 1097 snappedLeftInset() + snappedRightInset(); 1098 return snapSize(headersRegion.prefHeight(-1)) + padding; 1099 } 1100 1101 @Override public double getBaselineOffset() { 1102 if (getSkinnable().getSide() == Side.TOP) { 1103 return headersRegion.getBaselineOffset() + snappedTopInset(); 1104 } 1105 return 0; 1106 } 1107 1108 @Override protected void layoutChildren() { 1109 final double leftInset = snappedLeftInset(); 1110 final double rightInset = snappedRightInset(); 1111 final double topInset = snappedTopInset(); 1112 final double bottomInset = snappedBottomInset(); 1113 double w = snapSize(getWidth()) - (isHorizontal() ? 1114 leftInset + rightInset : topInset + bottomInset); 1115 double h = snapSize(getHeight()) - (isHorizontal() ? 1116 topInset + bottomInset : leftInset + rightInset); 1117 double tabBackgroundHeight = snapSize(prefHeight(-1)); 1118 double headersPrefWidth = snapSize(headersRegion.prefWidth(-1)); 1119 double headersPrefHeight = snapSize(headersRegion.prefHeight(-1)); 1120 1121 controlButtons.showTabsMenu(! tabsFit()); 1122 1123 updateHeaderClip(); 1124 headersRegion.requestLayout(); 1125 1126 // RESIZE CONTROL BUTTONS 1127 double btnWidth = snapSize(controlButtons.prefWidth(-1)); 1128 final double btnHeight = controlButtons.prefHeight(btnWidth); 1129 controlButtons.resize(btnWidth, btnHeight); 1130 1131 // POSITION TABS 1132 headersRegion.resize(headersPrefWidth, headersPrefHeight); 1133 1134 if (isFloatingStyleClass()) { 1135 headerBackground.setVisible(false); 1136 } else { 1137 headerBackground.resize(snapSize(getWidth()), snapSize(getHeight())); 1138 headerBackground.setVisible(true); 1139 } 1140 1141 double startX = 0; 1142 double startY = 0; 1143 double controlStartX = 0; 1144 double controlStartY = 0; 1145 Side tabPosition = getSkinnable().getSide(); 1146 1147 if (tabPosition.equals(Side.TOP)) { 1148 startX = leftInset; 1149 startY = tabBackgroundHeight - headersPrefHeight - bottomInset; 1150 controlStartX = w - btnWidth + leftInset; 1151 controlStartY = snapSize(getHeight()) - btnHeight - bottomInset; 1152 } else if (tabPosition.equals(Side.RIGHT)) { 1153 startX = topInset; 1154 startY = tabBackgroundHeight - headersPrefHeight - leftInset; 1155 controlStartX = w - btnWidth + topInset; 1156 controlStartY = snapSize(getHeight()) - btnHeight - leftInset; 1157 } else if (tabPosition.equals(Side.BOTTOM)) { 1158 startX = snapSize(getWidth()) - headersPrefWidth - leftInset; 1159 startY = tabBackgroundHeight - headersPrefHeight - topInset; 1160 controlStartX = rightInset; 1161 controlStartY = snapSize(getHeight()) - btnHeight - topInset; 1162 } else if (tabPosition.equals(Side.LEFT)) { 1163 startX = snapSize(getWidth()) - headersPrefWidth - topInset; 1164 startY = tabBackgroundHeight - headersPrefHeight - rightInset; 1165 controlStartX = leftInset; 1166 controlStartY = snapSize(getHeight()) - btnHeight - rightInset; 1167 } 1168 if (headerBackground.isVisible()) { 1169 positionInArea(headerBackground, 0, 0, 1170 snapSize(getWidth()), snapSize(getHeight()), /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1171 } 1172 positionInArea(headersRegion, startX, startY, w, h, /*baseline ignored*/0, HPos.LEFT, VPos.CENTER); 1173 positionInArea(controlButtons, controlStartX, controlStartY, btnWidth, btnHeight, 1174 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1175 } 1176 } /* End TabHeaderArea */ 1177 1178 1179 1180 1181 /************************************************************************** 1182 * 1183 * TabHeaderSkin: skin for each tab 1184 * 1185 **************************************************************************/ 1186 1187 class TabHeaderSkin extends StackPane { 1188 private final Tab tab; 1189 public Tab getTab() { 1190 return tab; 1191 } 1192 private Label label; 1193 private StackPane closeBtn; 1194 private StackPane inner; 1195 private Tooltip oldTooltip; 1196 private Tooltip tooltip; 1197 private Rectangle clip; 1198 1199 private boolean isClosing = false; 1200 1201 private LambdaMultiplePropertyChangeListenerHandler listener = new LambdaMultiplePropertyChangeListenerHandler(); 1202 1203 private final ListChangeListener<String> styleClassListener = new ListChangeListener<String>() { 1204 @Override 1205 public void onChanged(Change<? extends String> c) { 1206 getStyleClass().setAll(tab.getStyleClass()); 1207 } 1208 }; 1209 1210 private final WeakListChangeListener<String> weakStyleClassListener = 1211 new WeakListChangeListener<>(styleClassListener); 1212 1213 public TabHeaderSkin(final Tab tab) { 1214 getStyleClass().setAll(tab.getStyleClass()); 1215 setId(tab.getId()); 1216 setStyle(tab.getStyle()); 1217 setAccessibleRole(AccessibleRole.TAB_ITEM); 1218 setViewOrder(1); 1219 1220 this.tab = tab; 1221 clip = new Rectangle(); 1222 setClip(clip); 1223 1224 label = new Label(tab.getText(), tab.getGraphic()); 1225 label.getStyleClass().setAll("tab-label"); 1226 1227 closeBtn = new StackPane() { 1228 @Override protected double computePrefWidth(double h) { 1229 return CLOSE_BTN_SIZE; 1230 } 1231 @Override protected double computePrefHeight(double w) { 1232 return CLOSE_BTN_SIZE; 1233 } 1234 @Override 1235 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 1236 switch (action) { 1237 case FIRE: { 1238 Tab tab = getTab(); 1239 if (behavior.canCloseTab(tab)) { 1240 behavior.closeTab(tab); 1241 setOnMousePressed(null); 1242 } 1243 break; 1244 } 1245 default: super.executeAccessibleAction(action, parameters); 1246 } 1247 } 1248 }; 1249 closeBtn.setAccessibleRole(AccessibleRole.BUTTON); 1250 closeBtn.setAccessibleText(getString("Accessibility.title.TabPane.CloseButton")); 1251 closeBtn.getStyleClass().setAll("tab-close-button"); 1252 closeBtn.setOnMousePressed(new EventHandler<MouseEvent>() { 1253 @Override 1254 public void handle(MouseEvent me) { 1255 Tab tab = getTab(); 1256 if (behavior.canCloseTab(tab)) { 1257 behavior.closeTab(tab); 1258 setOnMousePressed(null); 1259 me.consume(); 1260 } 1261 } 1262 }); 1263 1264 updateGraphicRotation(); 1265 1266 final Region focusIndicator = new Region(); 1267 focusIndicator.setMouseTransparent(true); 1268 focusIndicator.getStyleClass().add("focus-indicator"); 1269 1270 inner = new StackPane() { 1271 @Override protected void layoutChildren() { 1272 final TabPane skinnable = getSkinnable(); 1273 1274 final double paddingTop = snappedTopInset(); 1275 final double paddingRight = snappedRightInset(); 1276 final double paddingBottom = snappedBottomInset(); 1277 final double paddingLeft = snappedLeftInset(); 1278 final double w = getWidth() - (paddingLeft + paddingRight); 1279 final double h = getHeight() - (paddingTop + paddingBottom); 1280 1281 final double prefLabelWidth = snapSize(label.prefWidth(-1)); 1282 final double prefLabelHeight = snapSize(label.prefHeight(-1)); 1283 1284 final double closeBtnWidth = showCloseButton() ? snapSize(closeBtn.prefWidth(-1)) : 0; 1285 final double closeBtnHeight = showCloseButton() ? snapSize(closeBtn.prefHeight(-1)) : 0; 1286 final double minWidth = snapSize(skinnable.getTabMinWidth()); 1287 final double maxWidth = snapSize(skinnable.getTabMaxWidth()); 1288 final double maxHeight = snapSize(skinnable.getTabMaxHeight()); 1289 1290 double labelAreaWidth = prefLabelWidth; 1291 double labelWidth = prefLabelWidth; 1292 double labelHeight = prefLabelHeight; 1293 1294 final double childrenWidth = labelAreaWidth + closeBtnWidth; 1295 final double childrenHeight = Math.max(labelHeight, closeBtnHeight); 1296 1297 if (childrenWidth > maxWidth && maxWidth != Double.MAX_VALUE) { 1298 labelAreaWidth = maxWidth - closeBtnWidth; 1299 labelWidth = maxWidth - closeBtnWidth; 1300 } else if (childrenWidth < minWidth) { 1301 labelAreaWidth = minWidth - closeBtnWidth; 1302 } 1303 1304 if (childrenHeight > maxHeight && maxHeight != Double.MAX_VALUE) { 1305 labelHeight = maxHeight; 1306 } 1307 1308 if (animationState != TabAnimationState.NONE) { 1309 // if (prefWidth.getValue() < labelAreaWidth) { 1310 // labelAreaWidth = prefWidth.getValue(); 1311 // } 1312 labelAreaWidth *= animationTransition.get(); 1313 closeBtn.setVisible(false); 1314 } else { 1315 closeBtn.setVisible(showCloseButton()); 1316 } 1317 1318 1319 label.resize(labelWidth, labelHeight); 1320 1321 1322 double labelStartX = paddingLeft; 1323 1324 // If maxWidth is less than Double.MAX_VALUE, the user has 1325 // clamped the max width, but we should 1326 // position the close button at the end of the tab, 1327 // which may not necessarily be the entire width of the 1328 // provided max width. 1329 double closeBtnStartX = (maxWidth < Double.MAX_VALUE ? Math.min(w, maxWidth) : w) - paddingRight - closeBtnWidth; 1330 1331 positionInArea(label, labelStartX, paddingTop, labelAreaWidth, h, 1332 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1333 1334 if (closeBtn.isVisible()) { 1335 closeBtn.resize(closeBtnWidth, closeBtnHeight); 1336 positionInArea(closeBtn, closeBtnStartX, paddingTop, closeBtnWidth, h, 1337 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1338 } 1339 1340 // Magic numbers regretfully introduced for RT-28944 (so that 1341 // the focus rect appears as expected on Windows and Mac). 1342 // In short we use the vPadding to shift the focus rect down 1343 // into the content area (whereas previously it was being clipped 1344 // on Windows, whilst it still looked fine on Mac). In the 1345 // future we may want to improve this code to remove the 1346 // magic number. Similarly, the hPadding differs on Mac. 1347 final int vPadding = Utils.isMac() ? 2 : 3; 1348 final int hPadding = Utils.isMac() ? 2 : 1; 1349 focusIndicator.resizeRelocate( 1350 paddingLeft - hPadding, 1351 paddingTop + vPadding, 1352 w + 2 * hPadding, 1353 h - 2 * vPadding); 1354 } 1355 }; 1356 inner.getStyleClass().add("tab-container"); 1357 inner.setRotate(getSkinnable().getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F); 1358 inner.getChildren().addAll(label, closeBtn, focusIndicator); 1359 1360 getChildren().addAll(inner); 1361 1362 tooltip = tab.getTooltip(); 1363 if (tooltip != null) { 1364 Tooltip.install(this, tooltip); 1365 oldTooltip = tooltip; 1366 } 1367 1368 listener.registerChangeListener(tab.closableProperty(), e -> { 1369 inner.requestLayout(); 1370 requestLayout(); 1371 }); 1372 listener.registerChangeListener(tab.selectedProperty(), e -> { 1373 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected()); 1374 // Need to request a layout pass for inner because if the width 1375 // and height didn't not change the label or close button may have 1376 // changed. 1377 inner.requestLayout(); 1378 requestLayout(); 1379 }); 1380 listener.registerChangeListener(tab.textProperty(),e -> label.setText(getTab().getText())); 1381 listener.registerChangeListener(tab.graphicProperty(), e -> label.setGraphic(getTab().getGraphic())); 1382 listener.registerChangeListener(tab.tooltipProperty(), e -> { 1383 // uninstall the old tooltip 1384 if (oldTooltip != null) { 1385 Tooltip.uninstall(this, oldTooltip); 1386 } 1387 tooltip = tab.getTooltip(); 1388 if (tooltip != null) { 1389 // install new tooltip and save as old tooltip. 1390 Tooltip.install(this, tooltip); 1391 oldTooltip = tooltip; 1392 } 1393 }); 1394 listener.registerChangeListener(tab.disabledProperty(), e -> { 1395 updateTabDisabledState(); 1396 }); 1397 listener.registerChangeListener(tab.getTabPane().disabledProperty(), e -> { 1398 updateTabDisabledState(); 1399 }); 1400 listener.registerChangeListener(tab.styleProperty(), e -> setStyle(tab.getStyle())); 1401 1402 tab.getStyleClass().addListener(weakStyleClassListener); 1403 1404 listener.registerChangeListener(getSkinnable().tabClosingPolicyProperty(),e -> { 1405 inner.requestLayout(); 1406 requestLayout(); 1407 }); 1408 listener.registerChangeListener(getSkinnable().sideProperty(),e -> { 1409 final Side side = getSkinnable().getSide(); 1410 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP)); 1411 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT)); 1412 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM)); 1413 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT)); 1414 inner.setRotate(side == Side.BOTTOM ? 180.0F : 0.0F); 1415 if (getSkinnable().isRotateGraphic()) { 1416 updateGraphicRotation(); 1417 } 1418 }); 1419 listener.registerChangeListener(getSkinnable().rotateGraphicProperty(), e -> updateGraphicRotation()); 1420 listener.registerChangeListener(getSkinnable().tabMinWidthProperty(), e -> { 1421 requestLayout(); 1422 getSkinnable().requestLayout(); 1423 }); 1424 listener.registerChangeListener(getSkinnable().tabMaxWidthProperty(), e -> { 1425 requestLayout(); 1426 getSkinnable().requestLayout(); 1427 }); 1428 listener.registerChangeListener(getSkinnable().tabMinHeightProperty(), e -> { 1429 requestLayout(); 1430 getSkinnable().requestLayout(); 1431 }); 1432 listener.registerChangeListener(getSkinnable().tabMaxHeightProperty(), e -> { 1433 requestLayout(); 1434 getSkinnable().requestLayout(); 1435 }); 1436 1437 getProperties().put(Tab.class, tab); 1438 getProperties().put(ContextMenu.class, tab.getContextMenu()); 1439 1440 setOnContextMenuRequested((ContextMenuEvent me) -> { 1441 if (getTab().getContextMenu() != null) { 1442 getTab().getContextMenu().show(inner, me.getScreenX(), me.getScreenY()); 1443 me.consume(); 1444 } 1445 }); 1446 setOnMousePressed(new EventHandler<MouseEvent>() { 1447 @Override public void handle(MouseEvent me) { 1448 if (getTab().isDisable()) { 1449 return; 1450 } 1451 if (me.getButton().equals(MouseButton.MIDDLE)) { 1452 if (showCloseButton()) { 1453 Tab tab = getTab(); 1454 if (behavior.canCloseTab(tab)) { 1455 removeListeners(tab); 1456 behavior.closeTab(tab); 1457 } 1458 } 1459 } else if (me.getButton().equals(MouseButton.PRIMARY)) { 1460 behavior.selectTab(getTab()); 1461 } 1462 } 1463 }); 1464 1465 // initialize pseudo-class state 1466 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected()); 1467 pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisabled()); 1468 final Side side = getSkinnable().getSide(); 1469 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP)); 1470 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT)); 1471 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM)); 1472 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT)); 1473 } 1474 1475 private void updateTabDisabledState() { 1476 pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisabled()); 1477 inner.requestLayout(); 1478 requestLayout(); 1479 } 1480 1481 private void updateGraphicRotation() { 1482 if (label.getGraphic() != null) { 1483 label.getGraphic().setRotate(getSkinnable().isRotateGraphic() ? 0.0F : 1484 (getSkinnable().getSide().equals(Side.RIGHT) ? -90.0F : 1485 (getSkinnable().getSide().equals(Side.LEFT) ? 90.0F : 0.0F))); 1486 } 1487 } 1488 1489 private boolean showCloseButton() { 1490 return tab.isClosable() && 1491 (getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.ALL_TABS) || 1492 getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.SELECTED_TAB) && tab.isSelected()); 1493 } 1494 1495 private final DoubleProperty animationTransition = new SimpleDoubleProperty(this, "animationTransition", 1.0) { 1496 @Override protected void invalidated() { 1497 requestLayout(); 1498 } 1499 }; 1500 1501 private void removeListeners(Tab tab) { 1502 listener.dispose(); 1503 inner.getChildren().clear(); 1504 getChildren().clear(); 1505 setOnContextMenuRequested(null); 1506 setOnMousePressed(null); 1507 } 1508 1509 private TabAnimationState animationState = TabAnimationState.NONE; 1510 private Timeline currentAnimation; 1511 1512 @Override protected double computePrefWidth(double height) { 1513 // if (animating) { 1514 // return prefWidth.getValue(); 1515 // } 1516 double minWidth = snapSize(getSkinnable().getTabMinWidth()); 1517 double maxWidth = snapSize(getSkinnable().getTabMaxWidth()); 1518 double paddingRight = snappedRightInset(); 1519 double paddingLeft = snappedLeftInset(); 1520 double tmpPrefWidth = snapSize(label.prefWidth(-1)); 1521 1522 // only include the close button width if it is relevant 1523 if (showCloseButton()) { 1524 tmpPrefWidth += snapSize(closeBtn.prefWidth(-1)); 1525 } 1526 1527 if (tmpPrefWidth > maxWidth) { 1528 tmpPrefWidth = maxWidth; 1529 } else if (tmpPrefWidth < minWidth) { 1530 tmpPrefWidth = minWidth; 1531 } 1532 tmpPrefWidth += paddingRight + paddingLeft; 1533 // prefWidth.setValue(tmpPrefWidth); 1534 return tmpPrefWidth; 1535 } 1536 1537 @Override protected double computePrefHeight(double width) { 1538 double minHeight = snapSize(getSkinnable().getTabMinHeight()); 1539 double maxHeight = snapSize(getSkinnable().getTabMaxHeight()); 1540 double paddingTop = snappedTopInset(); 1541 double paddingBottom = snappedBottomInset(); 1542 double tmpPrefHeight = snapSize(label.prefHeight(width)); 1543 1544 if (tmpPrefHeight > maxHeight) { 1545 tmpPrefHeight = maxHeight; 1546 } else if (tmpPrefHeight < minHeight) { 1547 tmpPrefHeight = minHeight; 1548 } 1549 tmpPrefHeight += paddingTop + paddingBottom; 1550 return tmpPrefHeight; 1551 } 1552 1553 @Override protected void layoutChildren() { 1554 double w = (snapSize(getWidth()) - snappedRightInset() - snappedLeftInset()) * animationTransition.getValue(); 1555 inner.resize(w, snapSize(getHeight()) - snappedTopInset() - snappedBottomInset()); 1556 inner.relocate(snappedLeftInset(), snappedTopInset()); 1557 } 1558 1559 @Override protected void setWidth(double value) { 1560 super.setWidth(value); 1561 clip.setWidth(value); 1562 } 1563 1564 @Override protected void setHeight(double value) { 1565 super.setHeight(value); 1566 clip.setHeight(value); 1567 } 1568 1569 /** {@inheritDoc} */ 1570 @Override 1571 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1572 switch (attribute) { 1573 case TEXT: return getTab().getText(); 1574 case SELECTED: return selectedTab == getTab(); 1575 default: return super.queryAccessibleAttribute(attribute, parameters); 1576 } 1577 } 1578 1579 /** {@inheritDoc} */ 1580 @Override 1581 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 1582 switch (action) { 1583 case REQUEST_FOCUS: 1584 getSkinnable().getSelectionModel().select(getTab()); 1585 break; 1586 default: super.executeAccessibleAction(action, parameters); 1587 } 1588 } 1589 1590 } /* End TabHeaderSkin */ 1591 1592 private static final PseudoClass SELECTED_PSEUDOCLASS_STATE = 1593 PseudoClass.getPseudoClass("selected"); 1594 private static final PseudoClass TOP_PSEUDOCLASS_STATE = 1595 PseudoClass.getPseudoClass("top"); 1596 private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE = 1597 PseudoClass.getPseudoClass("bottom"); 1598 private static final PseudoClass LEFT_PSEUDOCLASS_STATE = 1599 PseudoClass.getPseudoClass("left"); 1600 private static final PseudoClass RIGHT_PSEUDOCLASS_STATE = 1601 PseudoClass.getPseudoClass("right"); 1602 private static final PseudoClass DISABLED_PSEUDOCLASS_STATE = 1603 PseudoClass.getPseudoClass("disabled"); 1604 1605 1606 /************************************************************************** 1607 * 1608 * TabContentRegion: each tab has one to contain the tab's content node 1609 * 1610 **************************************************************************/ 1611 static class TabContentRegion extends StackPane { 1612 1613 private Tab tab; 1614 1615 private InvalidationListener tabContentListener = valueModel -> { 1616 updateContent(); 1617 }; 1618 private InvalidationListener tabSelectedListener = new InvalidationListener() { 1619 @Override public void invalidated(Observable valueModel) { 1620 setVisible(tab.isSelected()); 1621 } 1622 }; 1623 1624 private WeakInvalidationListener weakTabContentListener = 1625 new WeakInvalidationListener(tabContentListener); 1626 private WeakInvalidationListener weakTabSelectedListener = 1627 new WeakInvalidationListener(tabSelectedListener); 1628 1629 public Tab getTab() { 1630 return tab; 1631 } 1632 1633 public TabContentRegion(Tab tab) { 1634 getStyleClass().setAll("tab-content-area"); 1635 setManaged(false); 1636 this.tab = tab; 1637 updateContent(); 1638 setVisible(tab.isSelected()); 1639 1640 tab.selectedProperty().addListener(weakTabSelectedListener); 1641 tab.contentProperty().addListener(weakTabContentListener); 1642 } 1643 1644 private void updateContent() { 1645 Node newContent = getTab().getContent(); 1646 if (newContent == null) { 1647 getChildren().clear(); 1648 } else { 1649 getChildren().setAll(newContent); 1650 } 1651 } 1652 1653 private void removeListeners(Tab tab) { 1654 tab.selectedProperty().removeListener(weakTabSelectedListener); 1655 tab.contentProperty().removeListener(weakTabContentListener); 1656 } 1657 1658 } /* End TabContentRegion */ 1659 1660 /************************************************************************** 1661 * 1662 * TabControlButtons: controls to manipulate tab interaction 1663 * 1664 **************************************************************************/ 1665 class TabControlButtons extends StackPane { 1666 private StackPane inner; 1667 private StackPane downArrow; 1668 private Pane downArrowBtn; 1669 private boolean showControlButtons; 1670 private ContextMenu popup; 1671 1672 public TabControlButtons() { 1673 getStyleClass().setAll("control-buttons-tab"); 1674 1675 TabPane tabPane = getSkinnable(); 1676 1677 downArrowBtn = new Pane(); 1678 downArrowBtn.getStyleClass().setAll("tab-down-button"); 1679 downArrowBtn.setVisible(isShowTabsMenu()); 1680 downArrow = new StackPane(); 1681 downArrow.setManaged(false); 1682 downArrow.getStyleClass().setAll("arrow"); 1683 downArrow.setRotate(tabPane.getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F); 1684 downArrowBtn.getChildren().add(downArrow); 1685 downArrowBtn.setOnMouseClicked(me -> { 1686 showPopupMenu(); 1687 }); 1688 1689 setupPopupMenu(); 1690 1691 inner = new StackPane() { 1692 @Override protected double computePrefWidth(double height) { 1693 double pw; 1694 double maxArrowWidth = ! isShowTabsMenu() ? 0 : snapSize(downArrow.prefWidth(getHeight())) + snapSize(downArrowBtn.prefWidth(getHeight())); 1695 pw = 0.0F; 1696 if (isShowTabsMenu()) { 1697 pw += maxArrowWidth; 1698 } 1699 if (pw > 0) { 1700 pw += snappedLeftInset() + snappedRightInset(); 1701 } 1702 return pw; 1703 } 1704 1705 @Override protected double computePrefHeight(double width) { 1706 double height = 0.0F; 1707 if (isShowTabsMenu()) { 1708 height = Math.max(height, snapSize(downArrowBtn.prefHeight(width))); 1709 } 1710 if (height > 0) { 1711 height += snappedTopInset() + snappedBottomInset(); 1712 } 1713 return height; 1714 } 1715 1716 @Override protected void layoutChildren() { 1717 if (isShowTabsMenu()) { 1718 double x = 0; 1719 double y = snappedTopInset(); 1720 double w = snapSize(getWidth()) - x + snappedLeftInset(); 1721 double h = snapSize(getHeight()) - y + snappedBottomInset(); 1722 positionArrow(downArrowBtn, downArrow, x, y, w, h); 1723 } 1724 } 1725 1726 private void positionArrow(Pane btn, StackPane arrow, double x, double y, double width, double height) { 1727 btn.resize(width, height); 1728 positionInArea(btn, x, y, width, height, /*baseline ignored*/0, 1729 HPos.CENTER, VPos.CENTER); 1730 // center arrow region within arrow button 1731 double arrowWidth = snapSize(arrow.prefWidth(-1)); 1732 double arrowHeight = snapSize(arrow.prefHeight(-1)); 1733 arrow.resize(arrowWidth, arrowHeight); 1734 positionInArea(arrow, btn.snappedLeftInset(), btn.snappedTopInset(), 1735 width - btn.snappedLeftInset() - btn.snappedRightInset(), 1736 height - btn.snappedTopInset() - btn.snappedBottomInset(), 1737 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1738 } 1739 }; 1740 inner.getStyleClass().add("container"); 1741 inner.getChildren().add(downArrowBtn); 1742 1743 getChildren().add(inner); 1744 1745 tabPane.sideProperty().addListener(valueModel -> { 1746 Side tabPosition = getSkinnable().getSide(); 1747 downArrow.setRotate(tabPosition.equals(Side.BOTTOM)? 180.0F : 0.0F); 1748 }); 1749 tabPane.getTabs().addListener((ListChangeListener<Tab>) c -> setupPopupMenu()); 1750 showControlButtons = false; 1751 if (isShowTabsMenu()) { 1752 showControlButtons = true; 1753 requestLayout(); 1754 } 1755 getProperties().put(ContextMenu.class, popup); 1756 } 1757 1758 private boolean showTabsMenu = false; 1759 1760 private void showTabsMenu(boolean value) { 1761 final boolean wasTabsMenuShowing = isShowTabsMenu(); 1762 this.showTabsMenu = value; 1763 1764 if (showTabsMenu && !wasTabsMenuShowing) { 1765 downArrowBtn.setVisible(true); 1766 showControlButtons = true; 1767 inner.requestLayout(); 1768 tabHeaderArea.requestLayout(); 1769 } else if (!showTabsMenu && wasTabsMenuShowing) { 1770 hideControlButtons(); 1771 } 1772 } 1773 1774 private boolean isShowTabsMenu() { 1775 return showTabsMenu; 1776 } 1777 1778 @Override protected double computePrefWidth(double height) { 1779 double pw = snapSize(inner.prefWidth(height)); 1780 if (pw > 0) { 1781 pw += snappedLeftInset() + snappedRightInset(); 1782 } 1783 return pw; 1784 } 1785 1786 @Override protected double computePrefHeight(double width) { 1787 return Math.max(getSkinnable().getTabMinHeight(), snapSize(inner.prefHeight(width))) + 1788 snappedTopInset() + snappedBottomInset(); 1789 } 1790 1791 @Override protected void layoutChildren() { 1792 double x = snappedLeftInset(); 1793 double y = snappedTopInset(); 1794 double w = snapSize(getWidth()) - x + snappedRightInset(); 1795 double h = snapSize(getHeight()) - y + snappedBottomInset(); 1796 1797 if (showControlButtons) { 1798 showControlButtons(); 1799 showControlButtons = false; 1800 } 1801 1802 inner.resize(w, h); 1803 positionInArea(inner, x, y, w, h, /*baseline ignored*/0, HPos.CENTER, VPos.BOTTOM); 1804 } 1805 1806 private void showControlButtons() { 1807 setVisible(true); 1808 if (popup == null) { 1809 setupPopupMenu(); 1810 } 1811 } 1812 1813 private void hideControlButtons() { 1814 // If the scroll arrows or tab menu is still visible we don't want 1815 // to hide it animate it back it. 1816 if (isShowTabsMenu()) { 1817 showControlButtons = true; 1818 } else { 1819 setVisible(false); 1820 popup.getItems().clear(); 1821 popup = null; 1822 } 1823 1824 // This needs to be called when we are in the left tabPosition 1825 // to allow for the clip offset to move properly (otherwise 1826 // it jumps too early - before the animation is done). 1827 requestLayout(); 1828 } 1829 1830 private void setupPopupMenu() { 1831 if (popup == null) { 1832 popup = new ContextMenu(); 1833 } 1834 popup.getItems().clear(); 1835 ToggleGroup group = new ToggleGroup(); 1836 ObservableList<RadioMenuItem> menuitems = FXCollections.<RadioMenuItem>observableArrayList(); 1837 for (final Tab tab : getSkinnable().getTabs()) { 1838 TabMenuItem item = new TabMenuItem(tab); 1839 item.setToggleGroup(group); 1840 item.setOnAction(t -> getSkinnable().getSelectionModel().select(tab)); 1841 menuitems.add(item); 1842 } 1843 popup.getItems().addAll(menuitems); 1844 } 1845 1846 private void showPopupMenu() { 1847 for (MenuItem mi: popup.getItems()) { 1848 TabMenuItem tmi = (TabMenuItem)mi; 1849 if (selectedTab.equals(tmi.getTab())) { 1850 tmi.setSelected(true); 1851 break; 1852 } 1853 } 1854 popup.show(downArrowBtn, Side.BOTTOM, 0, 0); 1855 } 1856 } /* End TabControlButtons*/ 1857 1858 static class TabMenuItem extends RadioMenuItem { 1859 Tab tab; 1860 1861 private InvalidationListener disableListener = new InvalidationListener() { 1862 @Override public void invalidated(Observable o) { 1863 setDisable(tab.isDisable()); 1864 } 1865 }; 1866 1867 private WeakInvalidationListener weakDisableListener = 1868 new WeakInvalidationListener(disableListener); 1869 1870 public TabMenuItem(final Tab tab) { 1871 super(tab.getText(), TabPaneSkin.clone(tab.getGraphic())); 1872 this.tab = tab; 1873 setDisable(tab.isDisable()); 1874 tab.disableProperty().addListener(weakDisableListener); 1875 textProperty().bind(tab.textProperty()); 1876 } 1877 1878 public Tab getTab() { 1879 return tab; 1880 } 1881 1882 public void dispose() { 1883 tab.disableProperty().removeListener(weakDisableListener); 1884 } 1885 } 1886 1887 @Override 1888 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1889 switch (attribute) { 1890 case FOCUS_ITEM: return tabHeaderArea.getTabHeaderSkin(selectedTab); 1891 case ITEM_COUNT: return tabHeaderArea.headersRegion.getChildren().size(); 1892 case ITEM_AT_INDEX: { 1893 Integer index = (Integer)parameters[0]; 1894 if (index == null) return null; 1895 return tabHeaderArea.headersRegion.getChildren().get(index); 1896 } 1897 default: return super.queryAccessibleAttribute(attribute, parameters); 1898 } 1899 } 1900 1901 // -------------------------- 1902 // Tab Reordering 1903 // -------------------------- 1904 private enum DragState { 1905 NONE, 1906 START, 1907 REORDER 1908 } 1909 private EventHandler<MouseEvent> headerDraggedHandler = this::handleHeaderDragged; 1910 private EventHandler<MouseEvent> headerMousePressedHandler = this::handleHeaderMousePressed; 1911 private EventHandler<MouseEvent> headerMouseReleasedHandler = this::handleHeaderMouseReleased; 1912 1913 private int dragTabHeaderIndex; 1914 private TabHeaderSkin dragTabHeader; 1915 private TabHeaderSkin dropTabHeader; 1916 private StackPane headersRegion; 1917 private DragState dragState; 1918 private final int MIN_TO_MAX = 1; 1919 private final int MAX_TO_MIN = -1; 1920 private int xLayoutDirection; 1921 private double dragEventPrevLoc; 1922 private int prevDragDirection = MIN_TO_MAX; 1923 private final double DRAG_DIST_THRESHOLD = 0.75; 1924 1925 // Reordering Animation 1926 private final double ANIM_DURATION = 120; 1927 private TabHeaderSkin dropAnimHeader; 1928 private Tab swapTab; 1929 private double dropHeaderSourceX; 1930 private double dropHeaderTransitionX; 1931 private final Animation dropHeaderAnim = new Transition() { 1932 { 1933 setInterpolator(Interpolator.EASE_BOTH); 1934 setCycleDuration(Duration.millis(ANIM_DURATION)); 1935 setOnFinished(event -> { 1936 completeHeaderReordering(); 1937 }); 1938 } 1939 protected void interpolate(double frac) { 1940 dropAnimHeader.setLayoutX(dropHeaderSourceX + dropHeaderTransitionX * frac); 1941 } 1942 }; 1943 private double dragHeaderStartX; 1944 private double dragHeaderDestX; 1945 private double dragHeaderSourceX; 1946 private double dragHeaderTransitionX; 1947 private final Animation dragHeaderAnim = new Transition() { 1948 { 1949 setInterpolator(Interpolator.EASE_OUT); 1950 setCycleDuration(Duration.millis(ANIM_DURATION)); 1951 setOnFinished(event -> { 1952 resetDrag(); 1953 }); 1954 } 1955 protected void interpolate(double frac) { 1956 dragTabHeader.setLayoutX(dragHeaderSourceX + dragHeaderTransitionX * frac); 1957 } 1958 }; 1959 1960 // Helper methods for managing the listeners based on TabDragPolicy. 1961 private void addReorderListeners(Node n) { 1962 n.addEventHandler(MouseEvent.MOUSE_PRESSED, headerMousePressedHandler); 1963 n.addEventHandler(MouseEvent.MOUSE_RELEASED, headerMouseReleasedHandler); 1964 n.addEventHandler(MouseEvent.MOUSE_DRAGGED, headerDraggedHandler); 1965 } 1966 1967 private void removeReorderListeners(Node n) { 1968 n.removeEventHandler(MouseEvent.MOUSE_PRESSED, headerMousePressedHandler); 1969 n.removeEventHandler(MouseEvent.MOUSE_RELEASED, headerMouseReleasedHandler); 1970 n.removeEventHandler(MouseEvent.MOUSE_DRAGGED, headerDraggedHandler); 1971 } 1972 1973 private ListChangeListener childListener = new ListChangeListener<Node>() { 1974 public void onChanged(Change<? extends Node> change) { 1975 while (change.next()) { 1976 if (change.wasAdded()) { 1977 for(Node n : change.getAddedSubList()) { 1978 addReorderListeners(n); 1979 } 1980 } 1981 if (change.wasRemoved()) { 1982 for(Node n : change.getRemoved()) { 1983 removeReorderListeners(n); 1984 } 1985 } 1986 } 1987 } 1988 }; 1989 1990 private void updateListeners() { 1991 if (getSkinnable().getTabDragPolicy() == TabDragPolicy.FIXED || 1992 getSkinnable().getTabDragPolicy() == null) { 1993 for (Node n : headersRegion.getChildren()) { 1994 removeReorderListeners(n); 1995 } 1996 headersRegion.getChildren().removeListener(childListener); 1997 } else if (getSkinnable().getTabDragPolicy() == TabDragPolicy.REORDER) { 1998 for (Node n : headersRegion.getChildren()) { 1999 addReorderListeners(n); 2000 } 2001 headersRegion.getChildren().addListener(childListener); 2002 } 2003 } 2004 2005 private void setupReordering(StackPane headersRegion) { 2006 dragState = DragState.NONE; 2007 this.headersRegion = headersRegion; 2008 updateListeners(); 2009 getSkinnable().tabDragPolicyProperty().addListener((observable, oldValue, newValue) -> { 2010 if (oldValue != newValue) { 2011 updateListeners(); 2012 } 2013 }); 2014 } 2015 2016 private void handleHeaderMousePressed(MouseEvent event) { 2017 ((StackPane)event.getSource()).setMouseTransparent(true); 2018 startDrag(event); 2019 } 2020 2021 private void handleHeaderMouseReleased(MouseEvent event) { 2022 ((StackPane)event.getSource()).setMouseTransparent(false); 2023 stopDrag(); 2024 event.consume(); 2025 } 2026 2027 private void handleHeaderDragged(MouseEvent event) { 2028 perfromDrag(event); 2029 } 2030 2031 private double getDragDelta(double curr, double prev) { 2032 if (getSkinnable().getSide().equals(Side.TOP) || 2033 getSkinnable().getSide().equals(Side.RIGHT)) { 2034 return curr - prev; 2035 } else { 2036 return prev - curr; 2037 } 2038 } 2039 2040 private int deriveTabHeaderLayoutXDirection() { 2041 if (getSkinnable().getSide().equals(Side.TOP) || 2042 getSkinnable().getSide().equals(Side.RIGHT)) { 2043 // TabHeaderSkin are laid out in left to right direction inside headersRegion 2044 return MIN_TO_MAX; 2045 } 2046 // TabHeaderSkin are laid out in right to left direction inside headersRegion 2047 return MAX_TO_MIN; 2048 } 2049 2050 private void perfromDrag(MouseEvent event) { 2051 int dragDirection; 2052 double dragHeaderNewLayoutX; 2053 Bounds dragHeaderBounds; 2054 Bounds dropHeaderBounds; 2055 double draggedDist; 2056 double mouseCurrentLoc = getHeaderRegionLocalX(event); 2057 double dragDelta = getDragDelta(mouseCurrentLoc, dragEventPrevLoc); 2058 2059 if (dragDelta > 0) { 2060 // Dragging the tab header towards higher indexed tab headers inside headersRegion. 2061 dragDirection = MIN_TO_MAX; 2062 } else { 2063 // Dragging the tab header towards lower indexed tab headers inside headersRegion. 2064 dragDirection = MAX_TO_MIN; 2065 } 2066 // Stop dropHeaderAnim if direction of drag is changed 2067 if (prevDragDirection != dragDirection) { 2068 stopAnim(dropHeaderAnim); 2069 prevDragDirection = dragDirection; 2070 } 2071 2072 dragHeaderNewLayoutX = dragTabHeader.getLayoutX() + xLayoutDirection * dragDelta; 2073 2074 if (dragHeaderNewLayoutX >= 0 && 2075 dragHeaderNewLayoutX + dragTabHeader.getWidth() <= headersRegion.getWidth()) { 2076 2077 dragState = DragState.REORDER; 2078 dragTabHeader.setLayoutX(dragHeaderNewLayoutX); 2079 dragHeaderBounds = dragTabHeader.getBoundsInParent(); 2080 2081 if (dragDirection == MIN_TO_MAX) { 2082 // Dragging the tab header towards higher indexed tab headers 2083 // Last tab header can not be dragged outside headersRegion. 2084 2085 // When the mouse is moved too fast, sufficient number of events 2086 // are not generated. Hence it is required to check all possible 2087 // headers to be reordered. 2088 for (int i = dragTabHeaderIndex + 1; i < headersRegion.getChildren().size(); i++) { 2089 dropTabHeader = (TabHeaderSkin) headersRegion.getChildren().get(i); 2090 2091 // dropTabHeader should not be already reordering. 2092 if (dropAnimHeader != dropTabHeader) { 2093 dropHeaderBounds = dropTabHeader.getBoundsInParent(); 2094 2095 if (xLayoutDirection == MIN_TO_MAX) { 2096 draggedDist = dragHeaderBounds.getMaxX() - dropHeaderBounds.getMinX(); 2097 } else { 2098 draggedDist = dropHeaderBounds.getMaxX() - dragHeaderBounds.getMinX(); 2099 } 2100 2101 // A tab header is reordered when dragged tab header crosses DRAG_DIST_THRESHOLD% of next tab header's width. 2102 if (draggedDist > dropHeaderBounds.getWidth() * DRAG_DIST_THRESHOLD) { 2103 stopAnim(dropHeaderAnim); 2104 // Distance by which tab header should be animated. 2105 dropHeaderTransitionX = xLayoutDirection * -dragHeaderBounds.getWidth(); 2106 if (xLayoutDirection == MIN_TO_MAX) { 2107 dragHeaderDestX = dropHeaderBounds.getMaxX() - dragHeaderBounds.getWidth(); 2108 } else { 2109 dragHeaderDestX = dropHeaderBounds.getMinX(); 2110 } 2111 startHeaderReorderingAnim(); 2112 } else { 2113 break; 2114 } 2115 } 2116 } 2117 } else { 2118 // dragDirection is MAX_TO_MIN 2119 // Dragging the tab header towards lower indexed tab headers. 2120 // First tab header can not be dragged outside headersRegion. 2121 2122 // When the mouse is moved too fast, sufficient number of events 2123 // are not generated. Hence it is required to check all possible 2124 // tab headers to be reordered. 2125 for (int i = dragTabHeaderIndex - 1; i >= 0; i--) { 2126 dropTabHeader = (TabHeaderSkin) headersRegion.getChildren().get(i); 2127 2128 // dropTabHeader should not be already reordering. 2129 if (dropAnimHeader != dropTabHeader) { 2130 dropHeaderBounds = dropTabHeader.getBoundsInParent(); 2131 2132 if (xLayoutDirection == MIN_TO_MAX) { 2133 draggedDist = dropHeaderBounds.getMaxX() - dragHeaderBounds.getMinX(); 2134 } else { 2135 draggedDist = dragHeaderBounds.getMaxX() - dropHeaderBounds.getMinX(); 2136 } 2137 2138 // A tab header is reordered when dragged tab crosses DRAG_DIST_THRESHOLD% of next tab header's width. 2139 if (draggedDist > dropHeaderBounds.getWidth() * DRAG_DIST_THRESHOLD) { 2140 stopAnim(dropHeaderAnim); 2141 // Distance by which tab header should be animated. 2142 dropHeaderTransitionX = xLayoutDirection * dragHeaderBounds.getWidth(); 2143 if (xLayoutDirection == MIN_TO_MAX) { 2144 dragHeaderDestX = dropHeaderBounds.getMinX(); 2145 } else { 2146 dragHeaderDestX = dropHeaderBounds.getMaxX() - dragHeaderBounds.getWidth(); 2147 } 2148 startHeaderReorderingAnim(); 2149 } else { 2150 break; 2151 } 2152 } 2153 } 2154 } 2155 } 2156 dragEventPrevLoc = mouseCurrentLoc; 2157 event.consume(); 2158 } 2159 2160 private void startDrag(MouseEvent event) { 2161 // Stop the animations if any are running from previous reorder. 2162 stopAnim(dropHeaderAnim); 2163 stopAnim(dragHeaderAnim); 2164 2165 dragTabHeader = (TabHeaderSkin) event.getSource(); 2166 if (dragTabHeader != null) { 2167 dragState = DragState.START; 2168 swapTab = null; 2169 xLayoutDirection = deriveTabHeaderLayoutXDirection(); 2170 dragEventPrevLoc = getHeaderRegionLocalX(event); 2171 dragTabHeaderIndex = headersRegion.getChildren().indexOf(dragTabHeader); 2172 dragTabHeader.setViewOrder(0); 2173 dragHeaderStartX = dragHeaderDestX = dragTabHeader.getLayoutX(); 2174 } 2175 } 2176 2177 private double getHeaderRegionLocalX(MouseEvent ev) { 2178 // The event is converted to tab header's parent i.e. headersRegion's local space. 2179 // This will provide a value of X co-ordinate with all transformations of TabPane 2180 // and transformations of all nodes in the TabPane's parent hierarchy. 2181 Point2D sceneToLocalHR = headersRegion.sceneToLocal(ev.getSceneX(), ev.getSceneY()); 2182 return sceneToLocalHR.getX(); 2183 } 2184 2185 private void stopDrag() { 2186 if (dragState == DragState.START) { 2187 // No drag action was performed. 2188 resetDrag(); 2189 return; 2190 } 2191 // Animate tab header being dragged to its final position. 2192 dragHeaderSourceX = dragTabHeader.getLayoutX(); 2193 dragHeaderTransitionX = dragHeaderDestX - dragHeaderSourceX; 2194 dragHeaderAnim.playFromStart(); 2195 2196 // Reorder the tab list. 2197 if (dragHeaderStartX != dragHeaderDestX) { 2198 ((TabObservableList<Tab>) getSkinnable().getTabs()).reorder(dragTabHeader.tab, swapTab); 2199 swapTab = null; 2200 } 2201 } 2202 2203 private void resetDrag() { 2204 dragState = DragState.NONE; 2205 dragTabHeader.setViewOrder(1); 2206 dragTabHeader = null; 2207 dropTabHeader = null; 2208 headersRegion.requestLayout(); 2209 } 2210 2211 // Animate tab header being dropped-on to its new position. 2212 private void startHeaderReorderingAnim() { 2213 dropAnimHeader = dropTabHeader; 2214 swapTab = dropAnimHeader.tab; 2215 dropHeaderSourceX = dropAnimHeader.getLayoutX(); 2216 dropHeaderAnim.playFromStart(); 2217 } 2218 2219 // Remove dropAnimHeader and add at the index position of dragTabHeader. 2220 private void completeHeaderReordering() { 2221 if (dropAnimHeader != null) { 2222 headersRegion.getChildren().remove(dropAnimHeader); 2223 headersRegion.getChildren().add(dragTabHeaderIndex, dropAnimHeader); 2224 dropAnimHeader = null; 2225 headersRegion.requestLayout(); 2226 dragTabHeaderIndex = headersRegion.getChildren().indexOf(dragTabHeader); 2227 } 2228 } 2229 2230 // Helper method to stop an animation. 2231 private void stopAnim(Animation anim) { 2232 if (anim.getStatus() == Animation.Status.RUNNING) { 2233 anim.getOnFinished().handle(null); 2234 anim.stop(); 2235 } 2236 } 2237 }