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