6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package com.sun.javafx.scene.control.skin;
27
28 import com.sun.javafx.util.Utils;
29 import javafx.animation.Animation;
30 import javafx.animation.Interpolator;
31 import javafx.animation.KeyFrame;
32 import javafx.animation.KeyValue;
33 import javafx.animation.Timeline;
34 import javafx.beans.InvalidationListener;
35 import javafx.beans.Observable;
36 import javafx.beans.WeakInvalidationListener;
37 import javafx.beans.property.DoubleProperty;
38 import javafx.beans.property.ObjectProperty;
39 import javafx.beans.property.SimpleDoubleProperty;
40 import javafx.beans.value.WritableValue;
41 import javafx.collections.FXCollections;
42 import javafx.collections.ListChangeListener;
43 import javafx.collections.ObservableList;
44 import javafx.collections.WeakListChangeListener;
45 import javafx.css.CssMetaData;
46 import javafx.css.PseudoClass;
47 import javafx.css.Styleable;
48 import javafx.css.StyleableObjectProperty;
49 import javafx.css.StyleableProperty;
50 import javafx.event.ActionEvent;
51 import javafx.event.EventHandler;
52 import javafx.geometry.HPos;
53 import javafx.geometry.Pos;
54 import javafx.geometry.Side;
55 import javafx.geometry.VPos;
56 import javafx.scene.AccessibleAction;
57 import javafx.scene.AccessibleAttribute;
58 import javafx.scene.AccessibleRole;
59 import javafx.scene.Node;
60 import javafx.scene.control.ContextMenu;
61 import javafx.scene.control.Label;
62 import javafx.scene.control.MenuItem;
63 import javafx.scene.control.RadioMenuItem;
64 import javafx.scene.control.SkinBase;
65 import javafx.scene.control.Tab;
66 import javafx.scene.control.TabPane;
67 import javafx.scene.control.TabPane.TabClosingPolicy;
68 import javafx.scene.control.ToggleGroup;
69 import javafx.scene.control.Tooltip;
70 import javafx.scene.effect.DropShadow;
71 import javafx.scene.image.ImageView;
72 import javafx.scene.input.ContextMenuEvent;
73 import javafx.scene.input.MouseButton;
74 import javafx.scene.input.MouseEvent;
75 import javafx.scene.input.ScrollEvent;
76 import javafx.scene.input.SwipeEvent;
77 import javafx.scene.layout.Pane;
78 import javafx.scene.layout.Region;
79 import javafx.scene.layout.StackPane;
80 import javafx.scene.shape.Rectangle;
81 import javafx.scene.transform.Rotate;
82 import javafx.util.Duration;
83
84 import java.util.ArrayList;
85 import java.util.Collections;
86 import java.util.Iterator;
87 import java.util.List;
88
89 import com.sun.javafx.css.converters.EnumConverter;
90 import com.sun.javafx.scene.control.MultiplePropertyChangeListenerHandler;
91 import com.sun.javafx.scene.control.behavior.TabPaneBehavior;
92 import com.sun.javafx.scene.traversal.Direction;
93 import com.sun.javafx.scene.traversal.TraversalEngine;
94
95 import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString;
96
97 public class TabPaneSkin extends BehaviorSkinBase<TabPane, TabPaneBehavior> {
98 private static enum TabAnimation {
99 NONE,
100 GROW
101 // In future we could add FADE, ...
102 }
103
104 private enum TabAnimationState {
105 SHOWING, HIDING, NONE;
106 }
107
108 private ObjectProperty<TabAnimation> openTabAnimation = new StyleableObjectProperty<TabAnimation>(TabAnimation.GROW) {
109 @Override public CssMetaData<TabPane,TabAnimation> getCssMetaData() {
110 return StyleableProperties.OPEN_TAB_ANIMATION;
111 }
112
113 @Override public Object getBean() {
114 return TabPaneSkin.this;
115 }
116
117 @Override public String getName() {
118 return "openTabAnimation";
119 }
120 };
121
122 private ObjectProperty<TabAnimation> closeTabAnimation = new StyleableObjectProperty<TabAnimation>(TabAnimation.GROW) {
123 @Override public CssMetaData<TabPane,TabAnimation> getCssMetaData() {
124 return StyleableProperties.CLOSE_TAB_ANIMATION;
125 }
126
127 @Override public Object getBean() {
128 return TabPaneSkin.this;
129 }
130
131 @Override public String getName() {
132 return "closeTabAnimation";
133 }
134 };
135
136 private static int getRotation(Side pos) {
137 switch (pos) {
138 case TOP:
139 return 0;
140 case BOTTOM:
141 return 180;
142 case LEFT:
143 return -90;
144 case RIGHT:
145 return 90;
146 default:
147 return 0;
148 }
149 }
150
151 /**
152 * VERY HACKY - this lets us 'duplicate' Label and ImageView nodes to be used in a
153 * Tab and the tabs menu at the same time.
154 */
155 private static Node clone(Node n) {
156 if (n == null) {
157 return null;
158 }
159 if (n instanceof ImageView) {
160 ImageView iv = (ImageView) n;
161 ImageView imageview = new ImageView();
162 imageview.setImage(iv.getImage());
163 return imageview;
164 }
165 if (n instanceof Label) {
166 Label l = (Label)n;
167 Label label = new Label(l.getText(), l.getGraphic());
168 return label;
169 }
170 return null;
171 }
172 private static final double ANIMATION_SPEED = 150;
173 private static final int SPACER = 10;
174
175 private TabHeaderArea tabHeaderArea;
176 private ObservableList<TabContentRegion> tabContentRegions;
177 private Rectangle clipRect;
178 private Rectangle tabHeaderAreaClipRect;
179 private Tab selectedTab;
180 private boolean isSelectingTab;
181
182 public TabPaneSkin(TabPane tabPane) {
183 super(tabPane, new TabPaneBehavior(tabPane));
184
185 clipRect = new Rectangle(tabPane.getWidth(), tabPane.getHeight());
186 getSkinnable().setClip(clipRect);
187
188 tabContentRegions = FXCollections.<TabContentRegion>observableArrayList();
189
190 for (Tab tab : getSkinnable().getTabs()) {
191 addTabContent(tab);
192 }
193
194 tabHeaderAreaClipRect = new Rectangle();
195 tabHeaderArea = new TabHeaderArea();
196 tabHeaderArea.setClip(tabHeaderAreaClipRect);
197 getChildren().add(tabHeaderArea);
198 if (getSkinnable().getTabs().size() == 0) {
199 tabHeaderArea.setVisible(false);
200 }
201
202 initializeTabListener();
203
204 registerChangeListener(tabPane.getSelectionModel().selectedItemProperty(), "SELECTED_TAB");
205 registerChangeListener(tabPane.sideProperty(), "SIDE");
206 registerChangeListener(tabPane.widthProperty(), "WIDTH");
207 registerChangeListener(tabPane.heightProperty(), "HEIGHT");
208
209 selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
210 // Could not find the selected tab try and get the selected tab using the selected index
211 if (selectedTab == null && getSkinnable().getSelectionModel().getSelectedIndex() != -1) {
212 getSkinnable().getSelectionModel().select(getSkinnable().getSelectionModel().getSelectedIndex());
213 selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
214 }
215 if (selectedTab == null) {
216 // getSelectedItem and getSelectedIndex failed select the first.
217 getSkinnable().getSelectionModel().selectFirst();
218 }
219 selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
220 isSelectingTab = false;
221
222 initializeSwipeHandlers();
223 }
224
225 public StackPane getSelectedTabContentRegion() {
226 for (TabContentRegion contentRegion : tabContentRegions) {
227 if (contentRegion.getTab().equals(selectedTab)) {
228 return contentRegion;
229 }
230 }
231 return null;
232 }
233
234 @Override protected void handleControlPropertyChanged(String property) {
235 super.handleControlPropertyChanged(property);
236 if ("SELECTED_TAB".equals(property)) {
237 isSelectingTab = true;
238 selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
239 getSkinnable().requestLayout();
240 } else if ("SIDE".equals(property)) {
241 updateTabPosition();
242 } else if ("WIDTH".equals(property)) {
243 clipRect.setWidth(getSkinnable().getWidth());
244 } else if ("HEIGHT".equals(property)) {
245 clipRect.setHeight(getSkinnable().getHeight());
246 }
247 }
248 private void removeTabs(List<? extends Tab> removedList) {
249 for (final Tab tab : removedList) {
250 stopCurrentAnimation(tab);
251 // Animate the tab removal
252 final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab);
253 if (tabRegion != null) {
254 tabRegion.isClosing = true;
255
256 tabRegion.removeListeners(tab);
257 removeTabContent(tab);
258
259 // remove the menu item from the popup menu
260 ContextMenu popupMenu = tabHeaderArea.controlButtons.popup;
261 TabMenuItem tabItem = null;
262 if (popupMenu != null) {
263 for (MenuItem item : popupMenu.getItems()) {
264 tabItem = (TabMenuItem) item;
265 if (tab == tabItem.getTab()) {
266 break;
267 }
386 openTabAnimation.set(prevOpenAnimation);
387 closeTabAnimation.set(prevCloseAnimation);
388 getSkinnable().getSelectionModel().select(selTab);
389 }
390
391 if (c.wasRemoved()) {
392 tabsToRemove.addAll(c.getRemoved());
393 }
394
395 if (c.wasAdded()) {
396 tabsToAdd.addAll(c.getAddedSubList());
397 insertPos = c.getFrom();
398 }
399 }
400
401 // now only remove the tabs that are not in the tabsToAdd list
402 tabsToRemove.removeAll(tabsToAdd);
403 removeTabs(tabsToRemove);
404
405 // and add in any new tabs (that we don't already have showing)
406 if (! tabsToAdd.isEmpty()) {
407 for (TabContentRegion tabContentRegion : tabContentRegions) {
408 Tab tab = tabContentRegion.getTab();
409 TabHeaderSkin tabHeader = tabHeaderArea.getTabHeaderSkin(tab);
410 if (!tabHeader.isClosing && tabsToAdd.contains(tabContentRegion.getTab())) {
411 tabsToAdd.remove(tabContentRegion.getTab());
412 }
413 }
414
415 addTabs(tabsToAdd, insertPos == -1 ? tabContentRegions.size() : insertPos);
416 }
417
418 // Fix for RT-34692
419 getSkinnable().requestLayout();
420 });
421 }
422
423 private void addTabContent(Tab tab) {
424 TabContentRegion tabContentRegion = new TabContentRegion(tab);
425 tabContentRegion.setClip(new Rectangle());
426 tabContentRegions.add(tabContentRegion);
446 }
447
448 private Timeline createTimeline(final TabHeaderSkin tabRegion, final Duration duration, final double endValue, final EventHandler<ActionEvent> func) {
449 Timeline timeline = new Timeline();
450 timeline.setCycleCount(1);
451
452 KeyValue keyValue = new KeyValue(tabRegion.animationTransition, endValue, Interpolator.LINEAR);
453 timeline.getKeyFrames().clear();
454 timeline.getKeyFrames().add(new KeyFrame(duration, keyValue));
455
456 timeline.setOnFinished(func);
457 return timeline;
458 }
459
460 private boolean isHorizontal() {
461 Side tabPosition = getSkinnable().getSide();
462 return Side.TOP.equals(tabPosition) || Side.BOTTOM.equals(tabPosition);
463 }
464
465 private void initializeSwipeHandlers() {
466 if (IS_TOUCH_SUPPORTED) {
467 getSkinnable().addEventHandler(SwipeEvent.SWIPE_LEFT, t -> {
468 getBehavior().selectNextTab();
469 });
470
471 getSkinnable().addEventHandler(SwipeEvent.SWIPE_RIGHT, t -> {
472 getBehavior().selectPreviousTab();
473 });
474 }
475 }
476
477 //TODO need to cache this.
478 private boolean isFloatingStyleClass() {
479 return getSkinnable().getStyleClass().contains(TabPane.STYLE_CLASS_FLOATING);
480 }
481
482 private double maxw = 0.0d;
483 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
484 // The TabPane can only be as wide as it widest content width.
485 for (TabContentRegion contentRegion: tabContentRegions) {
486 maxw = Math.max(maxw, snapSize(contentRegion.prefWidth(-1)));
487 }
488
489 final boolean isHorizontal = isHorizontal();
490 final double tabHeaderAreaSize = snapSize(isHorizontal ?
491 tabHeaderArea.prefWidth(-1) : tabHeaderArea.prefHeight(-1));
492
493 double prefWidth = isHorizontal ?
494 Math.max(maxw, tabHeaderAreaSize) : maxw + tabHeaderAreaSize;
495 return snapSize(prefWidth) + rightInset + leftInset;
496 }
497
498 private double maxh = 0.0d;
499 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
500 // The TabPane can only be as high as it highest content height.
501 for (TabContentRegion contentRegion: tabContentRegions) {
502 maxh = Math.max(maxh, snapSize(contentRegion.prefHeight(-1)));
503 }
504
505 final boolean isHorizontal = isHorizontal();
506 final double tabHeaderAreaSize = snapSize(isHorizontal ?
507 tabHeaderArea.prefHeight(-1) : tabHeaderArea.prefWidth(-1));
508
509 double prefHeight = isHorizontal ?
510 maxh + snapSize(tabHeaderAreaSize) : Math.max(maxh, tabHeaderAreaSize);
511 return snapSize(prefHeight) + topInset + bottomInset;
512 }
513
514 @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
515 Side tabPosition = getSkinnable().getSide();
516 if (tabPosition == Side.TOP) {
517 return tabHeaderArea.getBaselineOffset() + topInset;
518 }
519 return 0;
520 }
521
522 @Override protected void layoutChildren(final double x, final double y,
523 final double w, final double h) {
524 TabPane tabPane = getSkinnable();
525 Side tabPosition = tabPane.getSide();
526
527 double headerHeight = snapSize(tabHeaderArea.prefHeight(-1));
528 double tabsStartX = tabPosition.equals(Side.RIGHT)? x + w - headerHeight : x;
529 double tabsStartY = tabPosition.equals(Side.BOTTOM)? y + h - headerHeight : y;
530
531 if (tabPosition == Side.TOP) {
532 tabHeaderArea.resize(w, headerHeight);
533 tabHeaderArea.relocate(tabsStartX, tabsStartY);
534 tabHeaderArea.getTransforms().clear();
535 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.TOP)));
536 } else if (tabPosition == Side.BOTTOM) {
537 tabHeaderArea.resize(w, headerHeight);
538 tabHeaderArea.relocate(w, tabsStartY - headerHeight);
539 tabHeaderArea.getTransforms().clear();
540 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.BOTTOM), 0, headerHeight));
541 } else if (tabPosition == Side.LEFT) {
542 tabHeaderArea.resize(h, headerHeight);
543 tabHeaderArea.relocate(tabsStartX + headerHeight, h - headerHeight);
544 tabHeaderArea.getTransforms().clear();
545 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.LEFT), 0, headerHeight));
546 } else if (tabPosition == Side.RIGHT) {
547 tabHeaderArea.resize(h, headerHeight);
548 tabHeaderArea.relocate(tabsStartX, y - headerHeight);
549 tabHeaderArea.getTransforms().clear();
550 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.RIGHT), 0, headerHeight));
551 }
552
553 tabHeaderAreaClipRect.setX(0);
554 tabHeaderAreaClipRect.setY(0);
555 if (isHorizontal()) {
556 tabHeaderAreaClipRect.setWidth(w);
557 } else {
558 tabHeaderAreaClipRect.setWidth(h);
559 }
560 tabHeaderAreaClipRect.setHeight(headerHeight);
561
562 // ==================================
563 // position the tab content for the selected tab only
564 // ==================================
565 // if the tabs are on the left, the content needs to be indented
566 double contentStartX = 0;
567 double contentStartY = 0;
568
569 if (tabPosition == Side.TOP) {
570 contentStartX = x;
571 contentStartY = y + headerHeight;
572 if (isFloatingStyleClass()) {
573 // This is to hide the top border content
574 contentStartY -= 1;
575 }
576 } else if (tabPosition == Side.BOTTOM) {
577 contentStartX = x;
578 contentStartY = y;
579 if (isFloatingStyleClass()) {
580 // This is to hide the bottom border content
581 contentStartY = 1;
582 }
583 } else if (tabPosition == Side.LEFT) {
584 contentStartX = x + headerHeight;
585 contentStartY = y;
586 if (isFloatingStyleClass()) {
587 // This is to hide the left border content
588 contentStartX -= 1;
589 }
590 } else if (tabPosition == Side.RIGHT) {
591 contentStartX = x;
592 contentStartY = y;
593 if (isFloatingStyleClass()) {
594 // This is to hide the right border content
595 contentStartX = 1;
596 }
597 }
598
599 double contentWidth = w - (isHorizontal() ? 0 : headerHeight);
600 double contentHeight = h - (isHorizontal() ? headerHeight: 0);
601
602 for (int i = 0, max = tabContentRegions.size(); i < max; i++) {
603 TabContentRegion tabContent = tabContentRegions.get(i);
604
605 tabContent.setAlignment(Pos.TOP_LEFT);
606 if (tabContent.getClip() != null) {
607 ((Rectangle)tabContent.getClip()).setWidth(contentWidth);
608 ((Rectangle)tabContent.getClip()).setHeight(contentHeight);
609 }
610
611 // we need to size all tabs, even if they aren't visible. For example,
612 // see RT-29167
613 tabContent.resize(contentWidth, contentHeight);
614 tabContent.relocate(contentStartX, contentStartY);
615 }
616 }
617
618
619 /**
620 * Super-lazy instantiation pattern from Bill Pugh.
621 * @treatAsPrivate implementation detail
622 */
623 private static class StyleableProperties {
624 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
625
626 private final static CssMetaData<TabPane,TabAnimation> OPEN_TAB_ANIMATION =
627 new CssMetaData<TabPane, TabPaneSkin.TabAnimation>("-fx-open-tab-animation",
628 new EnumConverter<TabAnimation>(TabAnimation.class), TabAnimation.GROW) {
629
630 @Override public boolean isSettable(TabPane node) {
631 return true;
632 }
633
634 @Override public StyleableProperty<TabAnimation> getStyleableProperty(TabPane node) {
635 TabPaneSkin skin = (TabPaneSkin) node.getSkin();
636 return (StyleableProperty<TabAnimation>)(WritableValue<TabAnimation>)skin.openTabAnimation;
637 }
646 }
647
648 @Override public StyleableProperty<TabAnimation> getStyleableProperty(TabPane node) {
649 TabPaneSkin skin = (TabPaneSkin) node.getSkin();
650 return (StyleableProperty<TabAnimation>)(WritableValue<TabAnimation>)skin.closeTabAnimation;
651 }
652 };
653
654 static {
655
656 final List<CssMetaData<? extends Styleable, ?>> styleables =
657 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
658 styleables.add(OPEN_TAB_ANIMATION);
659 styleables.add(CLOSE_TAB_ANIMATION);
660 STYLEABLES = Collections.unmodifiableList(styleables);
661
662 }
663 }
664
665 /**
666 * @return The CssMetaData associated with this class, which may include the
667 * CssMetaData of its super classes.
668 */
669 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
670 return StyleableProperties.STYLEABLES;
671 }
672
673 /**
674 * {@inheritDoc}
675 */
676 @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
677 return getClassCssMetaData();
678 }
679
680 /**************************************************************************
681 *
682 * TabHeaderArea: Area responsible for painting all tabs
683 *
684 **************************************************************************/
685 class TabHeaderArea extends StackPane {
686 private Rectangle headerClip;
687 private StackPane headersRegion;
688 private StackPane headerBackground;
689 private TabControlButtons controlButtons;
690
691 private boolean measureClosingTabs = false;
692
693 private double scrollOffset;
694
695 public TabHeaderArea() {
696 getStyleClass().setAll("tab-header-area");
697 setManaged(false);
698 final TabPane tabPane = getSkinnable();
699
1078 startX = snapSize(getWidth()) - headersPrefWidth - leftInset;
1079 startY = tabBackgroundHeight - headersPrefHeight - topInset;
1080 controlStartX = rightInset;
1081 controlStartY = snapSize(getHeight()) - btnHeight - topInset;
1082 } else if (tabPosition.equals(Side.LEFT)) {
1083 startX = snapSize(getWidth()) - headersPrefWidth - topInset;
1084 startY = tabBackgroundHeight - headersPrefHeight - rightInset;
1085 controlStartX = leftInset;
1086 controlStartY = snapSize(getHeight()) - btnHeight - rightInset;
1087 }
1088 if (headerBackground.isVisible()) {
1089 positionInArea(headerBackground, 0, 0,
1090 snapSize(getWidth()), snapSize(getHeight()), /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
1091 }
1092 positionInArea(headersRegion, startX, startY, w, h, /*baseline ignored*/0, HPos.LEFT, VPos.CENTER);
1093 positionInArea(controlButtons, controlStartX, controlStartY, btnWidth, btnHeight,
1094 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
1095 }
1096 } /* End TabHeaderArea */
1097
1098 static int CLOSE_BTN_SIZE = 16;
1099
1100 /**************************************************************************
1101 *
1102 * TabHeaderSkin: skin for each tab
1103 *
1104 **************************************************************************/
1105
1106 class TabHeaderSkin extends StackPane {
1107 private final Tab tab;
1108 public Tab getTab() {
1109 return tab;
1110 }
1111 private Label label;
1112 private StackPane closeBtn;
1113 private StackPane inner;
1114 private Tooltip oldTooltip;
1115 private Tooltip tooltip;
1116 private Rectangle clip;
1117
1118 private boolean isClosing = false;
1119
1120 private MultiplePropertyChangeListenerHandler listener =
1121 new MultiplePropertyChangeListenerHandler(param -> {
1122 handlePropertyChanged(param);
1123 return null;
1124 });
1125
1126 private final ListChangeListener<String> styleClassListener = new ListChangeListener<String>() {
1127 @Override
1128 public void onChanged(Change<? extends String> c) {
1129 getStyleClass().setAll(tab.getStyleClass());
1130 }
1131 };
1132
1133 private final WeakListChangeListener<String> weakStyleClassListener =
1134 new WeakListChangeListener<>(styleClassListener);
1135
1136 public TabHeaderSkin(final Tab tab) {
1137 getStyleClass().setAll(tab.getStyleClass());
1138 setId(tab.getId());
1139 setStyle(tab.getStyle());
1140 setAccessibleRole(AccessibleRole.TAB_ITEM);
1141
1142 this.tab = tab;
1143 clip = new Rectangle();
1144 setClip(clip);
1145
1146 label = new Label(tab.getText(), tab.getGraphic());
1147 label.getStyleClass().setAll("tab-label");
1148
1149 closeBtn = new StackPane() {
1150 @Override protected double computePrefWidth(double h) {
1151 return CLOSE_BTN_SIZE;
1152 }
1153 @Override protected double computePrefHeight(double w) {
1154 return CLOSE_BTN_SIZE;
1155 }
1156 @Override
1157 public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
1158 switch (action) {
1159 case FIRE: {
1160 Tab tab = getTab();
1161 TabPaneBehavior behavior = getBehavior();
1162 if (behavior.canCloseTab(tab)) {
1163 behavior.closeTab(tab);
1164 setOnMousePressed(null);
1165 }
1166 }
1167 default: super.executeAccessibleAction(action, parameters);
1168 }
1169 }
1170 };
1171 closeBtn.setAccessibleRole(AccessibleRole.BUTTON);
1172 closeBtn.setAccessibleText(getString("Accessibility.title.TabPane.CloseButton"));
1173 closeBtn.getStyleClass().setAll("tab-close-button");
1174 closeBtn.setOnMousePressed(new EventHandler<MouseEvent>() {
1175 @Override public void handle(MouseEvent me) {
1176 Tab tab = getTab();
1177 TabPaneBehavior behavior = getBehavior();
1178 if (behavior.canCloseTab(tab)) {
1179 behavior.closeTab(tab);
1180 setOnMousePressed(null);
1181 }
1182 }
1183 });
1184
1185 updateGraphicRotation();
1186
1187 final Region focusIndicator = new Region();
1188 focusIndicator.setMouseTransparent(true);
1189 focusIndicator.getStyleClass().add("focus-indicator");
1190
1191 inner = new StackPane() {
1192 @Override protected void layoutChildren() {
1193 final TabPane skinnable = getSkinnable();
1194
1195 final double paddingTop = snappedTopInset();
1196 final double paddingRight = snappedRightInset();
1197 final double paddingBottom = snappedBottomInset();
1269 final int hPadding = Utils.isMac() ? 2 : 1;
1270 focusIndicator.resizeRelocate(
1271 paddingLeft - hPadding,
1272 paddingTop + vPadding,
1273 w + 2 * hPadding,
1274 h - 2 * vPadding);
1275 }
1276 };
1277 inner.getStyleClass().add("tab-container");
1278 inner.setRotate(getSkinnable().getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F);
1279 inner.getChildren().addAll(label, closeBtn, focusIndicator);
1280
1281 getChildren().addAll(inner);
1282
1283 tooltip = tab.getTooltip();
1284 if (tooltip != null) {
1285 Tooltip.install(this, tooltip);
1286 oldTooltip = tooltip;
1287 }
1288
1289 listener.registerChangeListener(tab.closableProperty(), "CLOSABLE");
1290 listener.registerChangeListener(tab.selectedProperty(), "SELECTED");
1291 listener.registerChangeListener(tab.textProperty(), "TEXT");
1292 listener.registerChangeListener(tab.graphicProperty(), "GRAPHIC");
1293 listener.registerChangeListener(tab.contextMenuProperty(), "CONTEXT_MENU");
1294 listener.registerChangeListener(tab.tooltipProperty(), "TOOLTIP");
1295 listener.registerChangeListener(tab.disableProperty(), "DISABLE");
1296 listener.registerChangeListener(tab.styleProperty(), "STYLE");
1297
1298 tab.getStyleClass().addListener(weakStyleClassListener);
1299
1300 listener.registerChangeListener(getSkinnable().tabClosingPolicyProperty(), "TAB_CLOSING_POLICY");
1301 listener.registerChangeListener(getSkinnable().sideProperty(), "SIDE");
1302 listener.registerChangeListener(getSkinnable().rotateGraphicProperty(), "ROTATE_GRAPHIC");
1303 listener.registerChangeListener(getSkinnable().tabMinWidthProperty(), "TAB_MIN_WIDTH");
1304 listener.registerChangeListener(getSkinnable().tabMaxWidthProperty(), "TAB_MAX_WIDTH");
1305 listener.registerChangeListener(getSkinnable().tabMinHeightProperty(), "TAB_MIN_HEIGHT");
1306 listener.registerChangeListener(getSkinnable().tabMaxHeightProperty(), "TAB_MAX_HEIGHT");
1307
1308 getProperties().put(Tab.class, tab);
1309 getProperties().put(ContextMenu.class, tab.getContextMenu());
1310
1311 setOnContextMenuRequested((ContextMenuEvent me) -> {
1312 if (getTab().getContextMenu() != null) {
1313 getTab().getContextMenu().show(inner, me.getScreenX(), me.getScreenY());
1314 me.consume();
1315 }
1316 });
1317 setOnMousePressed(new EventHandler<MouseEvent>() {
1318 @Override public void handle(MouseEvent me) {
1319 if (getTab().isDisable()) {
1320 return;
1321 }
1322 if (me.getButton().equals(MouseButton.MIDDLE)) {
1323 if (showCloseButton()) {
1324 Tab tab = getTab();
1325 TabPaneBehavior behavior = getBehavior();
1326 if (behavior.canCloseTab(tab)) {
1327 removeListeners(tab);
1328 behavior.closeTab(tab);
1329 }
1330 }
1331 } else if (me.getButton().equals(MouseButton.PRIMARY)) {
1332 getBehavior().selectTab(getTab());
1333 }
1334 }
1335 });
1336
1337 // initialize pseudo-class state
1338 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
1339 pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisable());
1340 final Side side = getSkinnable().getSide();
1341 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
1342 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
1343 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
1344 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
1345 }
1346
1347 private void handlePropertyChanged(final String p) {
1348 // --- Tab properties
1349 if ("CLOSABLE".equals(p)) {
1350 inner.requestLayout();
1351 requestLayout();
1352 } else if ("SELECTED".equals(p)) {
1353 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
1354 // Need to request a layout pass for inner because if the width
1355 // and height didn't not change the label or close button may have
1356 // changed.
1357 inner.requestLayout();
1358 requestLayout();
1359 } else if ("TEXT".equals(p)) {
1360 label.setText(getTab().getText());
1361 } else if ("GRAPHIC".equals(p)) {
1362 label.setGraphic(getTab().getGraphic());
1363 } else if ("CONTEXT_MENU".equals(p)) {
1364 // todo
1365 } else if ("TOOLTIP".equals(p)) {
1366 // uninstall the old tooltip
1367 if (oldTooltip != null) {
1368 Tooltip.uninstall(this, oldTooltip);
1369 }
1370 tooltip = tab.getTooltip();
1371 if (tooltip != null) {
1372 // install new tooltip and save as old tooltip.
1373 Tooltip.install(this, tooltip);
1374 oldTooltip = tooltip;
1375 }
1376 } else if ("DISABLE".equals(p)) {
1377 pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisable());
1378 inner.requestLayout();
1379 requestLayout();
1380 } else if ("STYLE".equals(p)) {
1381 setStyle(tab.getStyle());
1382 }
1383
1384 // --- Skinnable properties
1385 else if ("TAB_CLOSING_POLICY".equals(p)) {
1386 inner.requestLayout();
1387 requestLayout();
1388 } else if ("SIDE".equals(p)) {
1389 final Side side = getSkinnable().getSide();
1390 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
1391 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
1392 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
1393 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
1394 inner.setRotate(side == Side.BOTTOM ? 180.0F : 0.0F);
1395 if (getSkinnable().isRotateGraphic()) {
1396 updateGraphicRotation();
1397 }
1398 } else if ("ROTATE_GRAPHIC".equals(p)) {
1399 updateGraphicRotation();
1400 } else if ("TAB_MIN_WIDTH".equals(p)) {
1401 requestLayout();
1402 getSkinnable().requestLayout();
1403 } else if ("TAB_MAX_WIDTH".equals(p)) {
1404 requestLayout();
1405 getSkinnable().requestLayout();
1406 } else if ("TAB_MIN_HEIGHT".equals(p)) {
1407 requestLayout();
1408 getSkinnable().requestLayout();
1409 } else if ("TAB_MAX_HEIGHT".equals(p)) {
1410 requestLayout();
1411 getSkinnable().requestLayout();
1412 }
1413 }
1414
1415 private void updateGraphicRotation() {
1416 if (label.getGraphic() != null) {
1417 label.getGraphic().setRotate(getSkinnable().isRotateGraphic() ? 0.0F :
1418 (getSkinnable().getSide().equals(Side.RIGHT) ? -90.0F :
1419 (getSkinnable().getSide().equals(Side.LEFT) ? 90.0F : 0.0F)));
1420 }
1421 }
1422
1423 private boolean showCloseButton() {
1424 return tab.isClosable() &&
1425 (getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.ALL_TABS) ||
1426 getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.SELECTED_TAB) && tab.isSelected());
1427 }
1428
1429 private final DoubleProperty animationTransition = new SimpleDoubleProperty(this, "animationTransition", 1.0) {
1430 @Override protected void invalidated() {
1431 requestLayout();
1432 }
|
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package javafx.scene.control.skin;
27
28 import com.sun.javafx.scene.control.LambdaMultiplePropertyChangeListenerHandler;
29 import com.sun.javafx.scene.control.Properties;
30 import com.sun.javafx.util.Utils;
31 import javafx.animation.Animation;
32 import javafx.animation.Interpolator;
33 import javafx.animation.KeyFrame;
34 import javafx.animation.KeyValue;
35 import javafx.animation.Timeline;
36 import javafx.beans.InvalidationListener;
37 import javafx.beans.Observable;
38 import javafx.beans.WeakInvalidationListener;
39 import javafx.beans.property.DoubleProperty;
40 import javafx.beans.property.ObjectProperty;
41 import javafx.beans.property.SimpleDoubleProperty;
42 import javafx.beans.value.WritableValue;
43 import javafx.collections.FXCollections;
44 import javafx.collections.ListChangeListener;
45 import javafx.collections.ObservableList;
46 import javafx.collections.WeakListChangeListener;
47 import javafx.css.CssMetaData;
48 import javafx.css.PseudoClass;
49 import javafx.css.Styleable;
50 import javafx.css.StyleableObjectProperty;
51 import javafx.css.StyleableProperty;
52 import javafx.event.ActionEvent;
53 import javafx.event.EventHandler;
54 import javafx.geometry.HPos;
55 import javafx.geometry.Pos;
56 import javafx.geometry.Side;
57 import javafx.geometry.VPos;
58 import javafx.scene.AccessibleAction;
59 import javafx.scene.AccessibleAttribute;
60 import javafx.scene.AccessibleRole;
61 import javafx.scene.Node;
62 import javafx.scene.control.ContextMenu;
63 import javafx.scene.control.Control;
64 import javafx.scene.control.Label;
65 import javafx.scene.control.MenuItem;
66 import javafx.scene.control.RadioMenuItem;
67 import javafx.scene.control.SkinBase;
68 import javafx.scene.control.Tab;
69 import javafx.scene.control.TabPane;
70 import javafx.scene.control.TabPane.TabClosingPolicy;
71 import javafx.scene.control.ToggleGroup;
72 import javafx.scene.control.Tooltip;
73 import javafx.scene.effect.DropShadow;
74 import javafx.scene.image.ImageView;
75 import javafx.scene.input.ContextMenuEvent;
76 import javafx.scene.input.MouseButton;
77 import javafx.scene.input.MouseEvent;
78 import javafx.scene.input.ScrollEvent;
79 import javafx.scene.input.SwipeEvent;
80 import javafx.scene.layout.Pane;
81 import javafx.scene.layout.Region;
82 import javafx.scene.layout.StackPane;
83 import javafx.scene.shape.Rectangle;
84 import javafx.scene.transform.Rotate;
85 import javafx.util.Duration;
86
87 import java.util.ArrayList;
88 import java.util.Collections;
89 import java.util.Iterator;
90 import java.util.List;
91
92 import javafx.css.converter.EnumConverter;
93 import com.sun.javafx.scene.control.behavior.TabPaneBehavior;
94 import com.sun.javafx.scene.traversal.Direction;
95 import com.sun.javafx.scene.traversal.TraversalEngine;
96
97 import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString;
98
99 /**
100 * Default skin implementation for the {@link TabPane} control.
101 *
102 * @see TabPane
103 * @since 9
104 */
105 public class TabPaneSkin extends SkinBase<TabPane> {
106
107 /***************************************************************************
108 * *
109 * Enums *
110 * *
111 **************************************************************************/
112
113 private enum TabAnimation {
114 NONE,
115 GROW
116 // In future we could add FADE, ...
117 }
118
119 private enum TabAnimationState {
120 SHOWING, HIDING, NONE;
121 }
122
123
124
125 /***************************************************************************
126 * *
127 * Static fields *
128 * *
129 **************************************************************************/
130
131 static int CLOSE_BTN_SIZE = 16;
132
133
134
135 /***************************************************************************
136 * *
137 * Private fields *
138 * *
139 **************************************************************************/
140
141 private static final double ANIMATION_SPEED = 150;
142 private static final int SPACER = 10;
143
144 private TabHeaderArea tabHeaderArea;
145 private ObservableList<TabContentRegion> tabContentRegions;
146 private Rectangle clipRect;
147 private Rectangle tabHeaderAreaClipRect;
148 private Tab selectedTab;
149 private boolean isSelectingTab;
150 private double maxw = 0.0d;
151 private double maxh = 0.0d;
152
153 private final TabPaneBehavior behavior;
154
155
156
157 /***************************************************************************
158 * *
159 * Constructors *
160 * *
161 **************************************************************************/
162
163 /**
164 * Creates a new TabPaneSkin instance, installing the necessary child
165 * nodes into the Control {@link Control#getChildren() children} list, as
166 * well as the necessary input mappings for handling key, mouse, etc events.
167 *
168 * @param control The control that this skin should be installed onto.
169 */
170 public TabPaneSkin(TabPane control) {
171 super(control);
172
173 // install default input map for the TabPane control
174 this.behavior = new TabPaneBehavior(control);
175 // control.setInputMap(behavior.getInputMap());
176
177 clipRect = new Rectangle(control.getWidth(), control.getHeight());
178 getSkinnable().setClip(clipRect);
179
180 tabContentRegions = FXCollections.<TabContentRegion>observableArrayList();
181
182 for (Tab tab : getSkinnable().getTabs()) {
183 addTabContent(tab);
184 }
185
186 tabHeaderAreaClipRect = new Rectangle();
187 tabHeaderArea = new TabHeaderArea();
188 tabHeaderArea.setClip(tabHeaderAreaClipRect);
189 getChildren().add(tabHeaderArea);
190 if (getSkinnable().getTabs().size() == 0) {
191 tabHeaderArea.setVisible(false);
192 }
193
194 initializeTabListener();
195
196 registerChangeListener(control.getSelectionModel().selectedItemProperty(), e -> {
197 isSelectingTab = true;
198 selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
199 getSkinnable().requestLayout();
200 });
201 registerChangeListener(control.sideProperty(), e -> updateTabPosition());
202 registerChangeListener(control.widthProperty(), e -> clipRect.setWidth(getSkinnable().getWidth()));
203 registerChangeListener(control.heightProperty(), e -> clipRect.setHeight(getSkinnable().getHeight()));
204
205 selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
206 // Could not find the selected tab try and get the selected tab using the selected index
207 if (selectedTab == null && getSkinnable().getSelectionModel().getSelectedIndex() != -1) {
208 getSkinnable().getSelectionModel().select(getSkinnable().getSelectionModel().getSelectedIndex());
209 selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
210 }
211 if (selectedTab == null) {
212 // getSelectedItem and getSelectedIndex failed select the first.
213 getSkinnable().getSelectionModel().selectFirst();
214 }
215 selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
216 isSelectingTab = false;
217
218 initializeSwipeHandlers();
219 }
220
221
222
223 /***************************************************************************
224 * *
225 * Properties *
226 * *
227 **************************************************************************/
228
229 private ObjectProperty<TabAnimation> openTabAnimation = new StyleableObjectProperty<TabAnimation>(TabAnimation.GROW) {
230 @Override public CssMetaData<TabPane,TabAnimation> getCssMetaData() {
231 return StyleableProperties.OPEN_TAB_ANIMATION;
232 }
233
234 @Override public Object getBean() {
235 return TabPaneSkin.this;
236 }
237
238 @Override public String getName() {
239 return "openTabAnimation";
240 }
241 };
242
243 private ObjectProperty<TabAnimation> closeTabAnimation = new StyleableObjectProperty<TabAnimation>(TabAnimation.GROW) {
244 @Override public CssMetaData<TabPane,TabAnimation> getCssMetaData() {
245 return StyleableProperties.CLOSE_TAB_ANIMATION;
246 }
247
248 @Override public Object getBean() {
249 return TabPaneSkin.this;
250 }
251
252 @Override public String getName() {
253 return "closeTabAnimation";
254 }
255 };
256
257
258
259 /***************************************************************************
260 * *
261 * Public API *
262 * *
263 **************************************************************************/
264
265 /** {@inheritDoc} */
266 @Override public void dispose() {
267 super.dispose();
268
269 if (behavior != null) {
270 behavior.dispose();
271 }
272 }
273
274 /** {@inheritDoc} */
275 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
276 // The TabPane can only be as wide as it widest content width.
277 for (TabContentRegion contentRegion: tabContentRegions) {
278 maxw = Math.max(maxw, snapSize(contentRegion.prefWidth(-1)));
279 }
280
281 final boolean isHorizontal = isHorizontal();
282 final double tabHeaderAreaSize = snapSize(isHorizontal ?
283 tabHeaderArea.prefWidth(-1) : tabHeaderArea.prefHeight(-1));
284
285 double prefWidth = isHorizontal ?
286 Math.max(maxw, tabHeaderAreaSize) : maxw + tabHeaderAreaSize;
287 return snapSize(prefWidth) + rightInset + leftInset;
288 }
289
290 /** {@inheritDoc} */
291 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
292 // The TabPane can only be as high as it highest content height.
293 for (TabContentRegion contentRegion: tabContentRegions) {
294 maxh = Math.max(maxh, snapSize(contentRegion.prefHeight(-1)));
295 }
296
297 final boolean isHorizontal = isHorizontal();
298 final double tabHeaderAreaSize = snapSize(isHorizontal ?
299 tabHeaderArea.prefHeight(-1) : tabHeaderArea.prefWidth(-1));
300
301 double prefHeight = isHorizontal ?
302 maxh + snapSize(tabHeaderAreaSize) : Math.max(maxh, tabHeaderAreaSize);
303 return snapSize(prefHeight) + topInset + bottomInset;
304 }
305
306 /** {@inheritDoc} */
307 @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
308 Side tabPosition = getSkinnable().getSide();
309 if (tabPosition == Side.TOP) {
310 return tabHeaderArea.getBaselineOffset() + topInset;
311 }
312 return 0;
313 }
314
315 /** {@inheritDoc} */
316 @Override protected void layoutChildren(final double x, final double y,
317 final double w, final double h) {
318 TabPane tabPane = getSkinnable();
319 Side tabPosition = tabPane.getSide();
320
321 double headerHeight = snapSize(tabHeaderArea.prefHeight(-1));
322 double tabsStartX = tabPosition.equals(Side.RIGHT)? x + w - headerHeight : x;
323 double tabsStartY = tabPosition.equals(Side.BOTTOM)? y + h - headerHeight : y;
324
325 if (tabPosition == Side.TOP) {
326 tabHeaderArea.resize(w, headerHeight);
327 tabHeaderArea.relocate(tabsStartX, tabsStartY);
328 tabHeaderArea.getTransforms().clear();
329 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.TOP)));
330 } else if (tabPosition == Side.BOTTOM) {
331 tabHeaderArea.resize(w, headerHeight);
332 tabHeaderArea.relocate(w, tabsStartY - headerHeight);
333 tabHeaderArea.getTransforms().clear();
334 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.BOTTOM), 0, headerHeight));
335 } else if (tabPosition == Side.LEFT) {
336 tabHeaderArea.resize(h, headerHeight);
337 tabHeaderArea.relocate(tabsStartX + headerHeight, h - headerHeight);
338 tabHeaderArea.getTransforms().clear();
339 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.LEFT), 0, headerHeight));
340 } else if (tabPosition == Side.RIGHT) {
341 tabHeaderArea.resize(h, headerHeight);
342 tabHeaderArea.relocate(tabsStartX, y - headerHeight);
343 tabHeaderArea.getTransforms().clear();
344 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.RIGHT), 0, headerHeight));
345 }
346
347 tabHeaderAreaClipRect.setX(0);
348 tabHeaderAreaClipRect.setY(0);
349 if (isHorizontal()) {
350 tabHeaderAreaClipRect.setWidth(w);
351 } else {
352 tabHeaderAreaClipRect.setWidth(h);
353 }
354 tabHeaderAreaClipRect.setHeight(headerHeight);
355
356 // ==================================
357 // position the tab content for the selected tab only
358 // ==================================
359 // if the tabs are on the left, the content needs to be indented
360 double contentStartX = 0;
361 double contentStartY = 0;
362
363 if (tabPosition == Side.TOP) {
364 contentStartX = x;
365 contentStartY = y + headerHeight;
366 if (isFloatingStyleClass()) {
367 // This is to hide the top border content
368 contentStartY -= 1;
369 }
370 } else if (tabPosition == Side.BOTTOM) {
371 contentStartX = x;
372 contentStartY = y;
373 if (isFloatingStyleClass()) {
374 // This is to hide the bottom border content
375 contentStartY = 1;
376 }
377 } else if (tabPosition == Side.LEFT) {
378 contentStartX = x + headerHeight;
379 contentStartY = y;
380 if (isFloatingStyleClass()) {
381 // This is to hide the left border content
382 contentStartX -= 1;
383 }
384 } else if (tabPosition == Side.RIGHT) {
385 contentStartX = x;
386 contentStartY = y;
387 if (isFloatingStyleClass()) {
388 // This is to hide the right border content
389 contentStartX = 1;
390 }
391 }
392
393 double contentWidth = w - (isHorizontal() ? 0 : headerHeight);
394 double contentHeight = h - (isHorizontal() ? headerHeight: 0);
395
396 for (int i = 0, max = tabContentRegions.size(); i < max; i++) {
397 TabContentRegion tabContent = tabContentRegions.get(i);
398
399 tabContent.setAlignment(Pos.TOP_LEFT);
400 if (tabContent.getClip() != null) {
401 ((Rectangle)tabContent.getClip()).setWidth(contentWidth);
402 ((Rectangle)tabContent.getClip()).setHeight(contentHeight);
403 }
404
405 // we need to size all tabs, even if they aren't visible. For example,
406 // see RT-29167
407 tabContent.resize(contentWidth, contentHeight);
408 tabContent.relocate(contentStartX, contentStartY);
409 }
410 }
411
412
413
414 /***************************************************************************
415 * *
416 * Private implementation *
417 * *
418 **************************************************************************/
419
420 private static int getRotation(Side pos) {
421 switch (pos) {
422 case TOP:
423 return 0;
424 case BOTTOM:
425 return 180;
426 case LEFT:
427 return -90;
428 case RIGHT:
429 return 90;
430 default:
431 return 0;
432 }
433 }
434
435 /**
436 * VERY HACKY - this lets us 'duplicate' Label and ImageView nodes to be used in a
437 * Tab and the tabs menu at the same time.
438 */
439 private static Node clone(Node n) {
440 if (n == null) {
441 return null;
442 }
443 if (n instanceof ImageView) {
444 ImageView iv = (ImageView) n;
445 ImageView imageview = new ImageView();
446 imageview.setImage(iv.getImage());
447 return imageview;
448 }
449 if (n instanceof Label) {
450 Label l = (Label)n;
451 Label label = new Label(l.getText(), l.getGraphic());
452 return label;
453 }
454 return null;
455 }
456
457 private void removeTabs(List<? extends Tab> removedList) {
458 for (final Tab tab : removedList) {
459 stopCurrentAnimation(tab);
460 // Animate the tab removal
461 final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab);
462 if (tabRegion != null) {
463 tabRegion.isClosing = true;
464
465 tabRegion.removeListeners(tab);
466 removeTabContent(tab);
467
468 // remove the menu item from the popup menu
469 ContextMenu popupMenu = tabHeaderArea.controlButtons.popup;
470 TabMenuItem tabItem = null;
471 if (popupMenu != null) {
472 for (MenuItem item : popupMenu.getItems()) {
473 tabItem = (TabMenuItem) item;
474 if (tab == tabItem.getTab()) {
475 break;
476 }
595 openTabAnimation.set(prevOpenAnimation);
596 closeTabAnimation.set(prevCloseAnimation);
597 getSkinnable().getSelectionModel().select(selTab);
598 }
599
600 if (c.wasRemoved()) {
601 tabsToRemove.addAll(c.getRemoved());
602 }
603
604 if (c.wasAdded()) {
605 tabsToAdd.addAll(c.getAddedSubList());
606 insertPos = c.getFrom();
607 }
608 }
609
610 // now only remove the tabs that are not in the tabsToAdd list
611 tabsToRemove.removeAll(tabsToAdd);
612 removeTabs(tabsToRemove);
613
614 // and add in any new tabs (that we don't already have showing)
615 if (!tabsToAdd.isEmpty()) {
616 for (TabContentRegion tabContentRegion : tabContentRegions) {
617 Tab tab = tabContentRegion.getTab();
618 TabHeaderSkin tabHeader = tabHeaderArea.getTabHeaderSkin(tab);
619 if (!tabHeader.isClosing && tabsToAdd.contains(tabContentRegion.getTab())) {
620 tabsToAdd.remove(tabContentRegion.getTab());
621 }
622 }
623
624 addTabs(tabsToAdd, insertPos == -1 ? tabContentRegions.size() : insertPos);
625 }
626
627 // Fix for RT-34692
628 getSkinnable().requestLayout();
629 });
630 }
631
632 private void addTabContent(Tab tab) {
633 TabContentRegion tabContentRegion = new TabContentRegion(tab);
634 tabContentRegion.setClip(new Rectangle());
635 tabContentRegions.add(tabContentRegion);
655 }
656
657 private Timeline createTimeline(final TabHeaderSkin tabRegion, final Duration duration, final double endValue, final EventHandler<ActionEvent> func) {
658 Timeline timeline = new Timeline();
659 timeline.setCycleCount(1);
660
661 KeyValue keyValue = new KeyValue(tabRegion.animationTransition, endValue, Interpolator.LINEAR);
662 timeline.getKeyFrames().clear();
663 timeline.getKeyFrames().add(new KeyFrame(duration, keyValue));
664
665 timeline.setOnFinished(func);
666 return timeline;
667 }
668
669 private boolean isHorizontal() {
670 Side tabPosition = getSkinnable().getSide();
671 return Side.TOP.equals(tabPosition) || Side.BOTTOM.equals(tabPosition);
672 }
673
674 private void initializeSwipeHandlers() {
675 if (Properties.IS_TOUCH_SUPPORTED) {
676 getSkinnable().addEventHandler(SwipeEvent.SWIPE_LEFT, t -> {
677 behavior.selectNextTab();
678 });
679
680 getSkinnable().addEventHandler(SwipeEvent.SWIPE_RIGHT, t -> {
681 behavior.selectPreviousTab();
682 });
683 }
684 }
685
686 //TODO need to cache this.
687 private boolean isFloatingStyleClass() {
688 return getSkinnable().getStyleClass().contains(TabPane.STYLE_CLASS_FLOATING);
689 }
690
691
692
693 /***************************************************************************
694 * *
695 * CSS *
696 * *
697 **************************************************************************/
698
699 /**
700 * Super-lazy instantiation pattern from Bill Pugh.
701 * @treatAsPrivate implementation detail
702 */
703 private static class StyleableProperties {
704 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
705
706 private final static CssMetaData<TabPane,TabAnimation> OPEN_TAB_ANIMATION =
707 new CssMetaData<TabPane, TabPaneSkin.TabAnimation>("-fx-open-tab-animation",
708 new EnumConverter<TabAnimation>(TabAnimation.class), TabAnimation.GROW) {
709
710 @Override public boolean isSettable(TabPane node) {
711 return true;
712 }
713
714 @Override public StyleableProperty<TabAnimation> getStyleableProperty(TabPane node) {
715 TabPaneSkin skin = (TabPaneSkin) node.getSkin();
716 return (StyleableProperty<TabAnimation>)(WritableValue<TabAnimation>)skin.openTabAnimation;
717 }
726 }
727
728 @Override public StyleableProperty<TabAnimation> getStyleableProperty(TabPane node) {
729 TabPaneSkin skin = (TabPaneSkin) node.getSkin();
730 return (StyleableProperty<TabAnimation>)(WritableValue<TabAnimation>)skin.closeTabAnimation;
731 }
732 };
733
734 static {
735
736 final List<CssMetaData<? extends Styleable, ?>> styleables =
737 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
738 styleables.add(OPEN_TAB_ANIMATION);
739 styleables.add(CLOSE_TAB_ANIMATION);
740 STYLEABLES = Collections.unmodifiableList(styleables);
741
742 }
743 }
744
745 /**
746 * Returns the CssMetaData associated with this class, which may include the
747 * CssMetaData of its super classes.
748 */
749 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
750 return StyleableProperties.STYLEABLES;
751 }
752
753 /**
754 * {@inheritDoc}
755 */
756 @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
757 return getClassCssMetaData();
758 }
759
760
761
762 /***************************************************************************
763 * *
764 * Support classes *
765 * *
766 **************************************************************************/
767
768 /**************************************************************************
769 *
770 * TabHeaderArea: Area responsible for painting all tabs
771 *
772 **************************************************************************/
773 class TabHeaderArea extends StackPane {
774 private Rectangle headerClip;
775 private StackPane headersRegion;
776 private StackPane headerBackground;
777 private TabControlButtons controlButtons;
778
779 private boolean measureClosingTabs = false;
780
781 private double scrollOffset;
782
783 public TabHeaderArea() {
784 getStyleClass().setAll("tab-header-area");
785 setManaged(false);
786 final TabPane tabPane = getSkinnable();
787
1166 startX = snapSize(getWidth()) - headersPrefWidth - leftInset;
1167 startY = tabBackgroundHeight - headersPrefHeight - topInset;
1168 controlStartX = rightInset;
1169 controlStartY = snapSize(getHeight()) - btnHeight - topInset;
1170 } else if (tabPosition.equals(Side.LEFT)) {
1171 startX = snapSize(getWidth()) - headersPrefWidth - topInset;
1172 startY = tabBackgroundHeight - headersPrefHeight - rightInset;
1173 controlStartX = leftInset;
1174 controlStartY = snapSize(getHeight()) - btnHeight - rightInset;
1175 }
1176 if (headerBackground.isVisible()) {
1177 positionInArea(headerBackground, 0, 0,
1178 snapSize(getWidth()), snapSize(getHeight()), /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
1179 }
1180 positionInArea(headersRegion, startX, startY, w, h, /*baseline ignored*/0, HPos.LEFT, VPos.CENTER);
1181 positionInArea(controlButtons, controlStartX, controlStartY, btnWidth, btnHeight,
1182 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
1183 }
1184 } /* End TabHeaderArea */
1185
1186
1187
1188
1189 /**************************************************************************
1190 *
1191 * TabHeaderSkin: skin for each tab
1192 *
1193 **************************************************************************/
1194
1195 class TabHeaderSkin extends StackPane {
1196 private final Tab tab;
1197 public Tab getTab() {
1198 return tab;
1199 }
1200 private Label label;
1201 private StackPane closeBtn;
1202 private StackPane inner;
1203 private Tooltip oldTooltip;
1204 private Tooltip tooltip;
1205 private Rectangle clip;
1206
1207 private boolean isClosing = false;
1208
1209 private LambdaMultiplePropertyChangeListenerHandler listener = new LambdaMultiplePropertyChangeListenerHandler();
1210
1211 private final ListChangeListener<String> styleClassListener = new ListChangeListener<String>() {
1212 @Override
1213 public void onChanged(Change<? extends String> c) {
1214 getStyleClass().setAll(tab.getStyleClass());
1215 }
1216 };
1217
1218 private final WeakListChangeListener<String> weakStyleClassListener =
1219 new WeakListChangeListener<>(styleClassListener);
1220
1221 public TabHeaderSkin(final Tab tab) {
1222 getStyleClass().setAll(tab.getStyleClass());
1223 setId(tab.getId());
1224 setStyle(tab.getStyle());
1225 setAccessibleRole(AccessibleRole.TAB_ITEM);
1226
1227 this.tab = tab;
1228 clip = new Rectangle();
1229 setClip(clip);
1230
1231 label = new Label(tab.getText(), tab.getGraphic());
1232 label.getStyleClass().setAll("tab-label");
1233
1234 closeBtn = new StackPane() {
1235 @Override protected double computePrefWidth(double h) {
1236 return CLOSE_BTN_SIZE;
1237 }
1238 @Override protected double computePrefHeight(double w) {
1239 return CLOSE_BTN_SIZE;
1240 }
1241 @Override
1242 public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
1243 switch (action) {
1244 case FIRE: {
1245 Tab tab = getTab();
1246 if (behavior.canCloseTab(tab)) {
1247 behavior.closeTab(tab);
1248 setOnMousePressed(null);
1249 }
1250 }
1251 default: super.executeAccessibleAction(action, parameters);
1252 }
1253 }
1254 };
1255 closeBtn.setAccessibleRole(AccessibleRole.BUTTON);
1256 closeBtn.setAccessibleText(getString("Accessibility.title.TabPane.CloseButton"));
1257 closeBtn.getStyleClass().setAll("tab-close-button");
1258 closeBtn.setOnMousePressed(new EventHandler<MouseEvent>() {
1259 @Override
1260 public void handle(MouseEvent me) {
1261 Tab tab = getTab();
1262 if (behavior.canCloseTab(tab)) {
1263 behavior.closeTab(tab);
1264 setOnMousePressed(null);
1265 }
1266 }
1267 });
1268
1269 updateGraphicRotation();
1270
1271 final Region focusIndicator = new Region();
1272 focusIndicator.setMouseTransparent(true);
1273 focusIndicator.getStyleClass().add("focus-indicator");
1274
1275 inner = new StackPane() {
1276 @Override protected void layoutChildren() {
1277 final TabPane skinnable = getSkinnable();
1278
1279 final double paddingTop = snappedTopInset();
1280 final double paddingRight = snappedRightInset();
1281 final double paddingBottom = snappedBottomInset();
1353 final int hPadding = Utils.isMac() ? 2 : 1;
1354 focusIndicator.resizeRelocate(
1355 paddingLeft - hPadding,
1356 paddingTop + vPadding,
1357 w + 2 * hPadding,
1358 h - 2 * vPadding);
1359 }
1360 };
1361 inner.getStyleClass().add("tab-container");
1362 inner.setRotate(getSkinnable().getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F);
1363 inner.getChildren().addAll(label, closeBtn, focusIndicator);
1364
1365 getChildren().addAll(inner);
1366
1367 tooltip = tab.getTooltip();
1368 if (tooltip != null) {
1369 Tooltip.install(this, tooltip);
1370 oldTooltip = tooltip;
1371 }
1372
1373 listener.registerChangeListener(tab.closableProperty(), e -> {
1374 inner.requestLayout();
1375 requestLayout();
1376 });
1377 listener.registerChangeListener(tab.selectedProperty(), e -> {
1378 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
1379 // Need to request a layout pass for inner because if the width
1380 // and height didn't not change the label or close button may have
1381 // changed.
1382 inner.requestLayout();
1383 requestLayout();
1384 });
1385 listener.registerChangeListener(tab.textProperty(),e -> label.setText(getTab().getText()));
1386 listener.registerChangeListener(tab.graphicProperty(), e -> label.setGraphic(getTab().getGraphic()));
1387 listener.registerChangeListener(tab.tooltipProperty(), e -> {
1388 // uninstall the old tooltip
1389 if (oldTooltip != null) {
1390 Tooltip.uninstall(this, oldTooltip);
1391 }
1392 tooltip = tab.getTooltip();
1393 if (tooltip != null) {
1394 // install new tooltip and save as old tooltip.
1395 Tooltip.install(this, tooltip);
1396 oldTooltip = tooltip;
1397 }
1398 });
1399 listener.registerChangeListener(tab.disableProperty(), e -> {
1400 pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisable());
1401 inner.requestLayout();
1402 requestLayout();
1403 });
1404 listener.registerChangeListener(tab.styleProperty(), e -> setStyle(tab.getStyle()));
1405
1406 tab.getStyleClass().addListener(weakStyleClassListener);
1407
1408 listener.registerChangeListener(getSkinnable().tabClosingPolicyProperty(),e -> {
1409 inner.requestLayout();
1410 requestLayout();
1411 });
1412 listener.registerChangeListener(getSkinnable().sideProperty(),e -> {
1413 final Side side = getSkinnable().getSide();
1414 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
1415 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
1416 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
1417 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
1418 inner.setRotate(side == Side.BOTTOM ? 180.0F : 0.0F);
1419 if (getSkinnable().isRotateGraphic()) {
1420 updateGraphicRotation();
1421 }
1422 });
1423 listener.registerChangeListener(getSkinnable().rotateGraphicProperty(), e -> updateGraphicRotation());
1424 listener.registerChangeListener(getSkinnable().tabMinWidthProperty(), e -> {
1425 requestLayout();
1426 getSkinnable().requestLayout();
1427 });
1428 listener.registerChangeListener(getSkinnable().tabMaxWidthProperty(), e -> {
1429 requestLayout();
1430 getSkinnable().requestLayout();
1431 });
1432 listener.registerChangeListener(getSkinnable().tabMinHeightProperty(), e -> {
1433 requestLayout();
1434 getSkinnable().requestLayout();
1435 });
1436 listener.registerChangeListener(getSkinnable().tabMaxHeightProperty(), e -> {
1437 requestLayout();
1438 getSkinnable().requestLayout();
1439 });
1440
1441 getProperties().put(Tab.class, tab);
1442 getProperties().put(ContextMenu.class, tab.getContextMenu());
1443
1444 setOnContextMenuRequested((ContextMenuEvent me) -> {
1445 if (getTab().getContextMenu() != null) {
1446 getTab().getContextMenu().show(inner, me.getScreenX(), me.getScreenY());
1447 me.consume();
1448 }
1449 });
1450 setOnMousePressed(new EventHandler<MouseEvent>() {
1451 @Override public void handle(MouseEvent me) {
1452 if (getTab().isDisable()) {
1453 return;
1454 }
1455 if (me.getButton().equals(MouseButton.MIDDLE)) {
1456 if (showCloseButton()) {
1457 Tab tab = getTab();
1458 if (behavior.canCloseTab(tab)) {
1459 removeListeners(tab);
1460 behavior.closeTab(tab);
1461 }
1462 }
1463 } else if (me.getButton().equals(MouseButton.PRIMARY)) {
1464 behavior.selectTab(getTab());
1465 }
1466 }
1467 });
1468
1469 // initialize pseudo-class state
1470 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
1471 pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisable());
1472 final Side side = getSkinnable().getSide();
1473 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
1474 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
1475 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
1476 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
1477 }
1478
1479 private void updateGraphicRotation() {
1480 if (label.getGraphic() != null) {
1481 label.getGraphic().setRotate(getSkinnable().isRotateGraphic() ? 0.0F :
1482 (getSkinnable().getSide().equals(Side.RIGHT) ? -90.0F :
1483 (getSkinnable().getSide().equals(Side.LEFT) ? 90.0F : 0.0F)));
1484 }
1485 }
1486
1487 private boolean showCloseButton() {
1488 return tab.isClosable() &&
1489 (getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.ALL_TABS) ||
1490 getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.SELECTED_TAB) && tab.isSelected());
1491 }
1492
1493 private final DoubleProperty animationTransition = new SimpleDoubleProperty(this, "animationTransition", 1.0) {
1494 @Override protected void invalidated() {
1495 requestLayout();
1496 }
|