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