--- old/apps/toys/Hello/src/main/java/hello/HelloTabPane.java 2017-12-01 17:43:50.822819000 +0530 +++ new/apps/toys/Hello/src/main/java/hello/HelloTabPane.java 2017-12-01 17:43:50.582819000 +0530 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2017, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,30 +28,39 @@ import javafx.application.Application; import javafx.beans.InvalidationListener; import javafx.beans.Observable; +import javafx.collections.ListChangeListener; import javafx.event.ActionEvent; import javafx.event.Event; import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.geometry.NodeOrientation; import javafx.geometry.Side; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.FlowPane; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; +import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; + public class HelloTabPane extends Application { private TabPane tabPane; @@ -61,6 +70,7 @@ private Tab emptyTab; private Tab internalTab; private Tab multipleTabs; + private Tab tabForDragPolicy; private ContextMenu menu; private boolean showScrollArrows = false; @@ -78,6 +88,7 @@ emptyTab = new Tab(); internalTab = new Tab(); multipleTabs = new Tab(); + tabForDragPolicy = new Tab(); setUpPopupMenu(); stage.setTitle("Hello TabPane2"); final Scene scene = new Scene(new Group(), 800, 800); @@ -306,6 +317,11 @@ tab3.setContent(vbox); tabPane.getTabs().add(tab3); } + { + tabForDragPolicy.setText("TabDragPolicy"); + tabForDragPolicy.setContent(setupDragPolicyTab()); + tabPane.getTabs().add(tabForDragPolicy); + } emptyTab.setText("Empty Tab"); emptyTab.setContent(new Region()); @@ -350,6 +366,293 @@ stage.show(); } + private VBox setupDragPolicyTab() { + Label indexErr = new Label(""); + Label angleErr = new Label(""); + VBox mainContent = new VBox(); + mainContent.setSpacing(20); + mainContent.setMinSize(1000, 400); + + TabPane tabPane = new TabPane(); + tabPane.setMinSize(500, 400); + tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.ALL_TABS); + HBox tabs = new HBox(); + tabs.getChildren().add(tabPane); + mainContent.getChildren().add(tabs); + + for (int i = 0; i < 7; ++i) { + String text = i + " "; + HBox hb = new HBox(); + Tab tab = new Tab(text); + TextArea ta = new TextArea("" + i + i + i + i); + ta.setStyle("-fx-max-width: 300; -fx-max-height: 100;"); + hb.getChildren().add(ta); + tab.setContent(hb); + tabPane.getTabs().add(tab); + } + + TextArea instructions = new TextArea( + "**** INSTRUCTIONS ****\n\n" + + "1. TabDragPolicy.FIXED : Default policy.\n" + + "Click the FIXED button to set FIXED TabDragPolicy.\n" + + "The tabs remain fixed & tab headers cannot be dragged to reorder.\n\n" + + "2. TabDragPolicy.REORDER :\n" + + "Click the REORDER button to set REORDER TabDragPolicy.\n" + + "The tabs can be reordered with mouse press & drag action on tab header.\n\n" + + "3. With each of the drag policy,\n" + + "Choose different combinations of\n" + + "sides (TOP or BOTTOM or LEFT or RIGHT),\n" + + "node orientations, (LTR or RTL) and\n" + + "different rotation angle.\n\n" + + "4. Perform reordering and verify the correctness of the\n" + + "printed Current order of tabs and permuted tabs.\n" + + "And verify the navigation of tabs using left, right arrow keys.\n\n" + + "5. Additionally, also verify the outputs with ADD, REMOVE\n" + + "and REVERSE buttons." + ); + tabs.getChildren().add(instructions); + + Label permuted = new Label("Permuted tabs : "); + Label outputPermutedTabs = new Label(); + Label added = new Label("Added : "); + Label outputAddedTabs = new Label(); + Label removed = new Label("Removed : "); + Label outputRemovedTabs = new Label(); + Label getTabs = new Label("Current order of Tabs : "); + Label outputListOfTabs = new Label(); + tabPane.setOnMousePressed(event -> { + outputPermutedTabs.setText(""); + outputAddedTabs.setText(""); + outputRemovedTabs.setText(""); + outputListOfTabs.setText(""); + angleErr.setText(""); + indexErr.setText(""); + }); + VBox notifications = new VBox(); + notifications.setSpacing(15); + notifications.setStyle("-fx-border-color: black"); + notifications.setPadding(new Insets(20)); + HBox permut = new HBox(); + permut.getChildren().addAll(permuted, outputPermutedTabs); + + HBox adds = new HBox(); + adds.getChildren().addAll(added, outputAddedTabs); + + HBox removes = new HBox(); + removes.getChildren().addAll(removed, outputRemovedTabs); + + HBox allTabs = new HBox(); + allTabs.getChildren().addAll(getTabs, outputListOfTabs); + notifications.getChildren().addAll(permut, adds, removes, allTabs); + mainContent.getChildren().add(notifications); + + tabPane.getTabs().addListener((ListChangeListener) c -> { + while (c.next()) { + String list = ""; + outputPermutedTabs.setText(""); + outputAddedTabs.setText(""); + outputRemovedTabs.setText(""); + for (int i = c.getFrom(); i < c.getTo(); i++) { + list += tabPane.getTabs().get(i).getText() + ", "; + } + + if (c.wasPermutated()) { + outputPermutedTabs.setText(list); + } else if (c.wasAdded()) { + outputAddedTabs.setText(list); + } else if (c.wasRemoved()) { + list = ""; + for (Tab t : c.getRemoved()) { + list += t.getText() + ", "; + } + outputRemovedTabs.setText(list); + } + list = ""; + for (Tab t : tabPane.getTabs()) { + list += t.getText() + ", "; + } + outputListOfTabs.setText(list); + } + }); + + HBox bottomBox = new HBox(); + bottomBox.setSpacing(15); + bottomBox.setPadding(new Insets(20)); + bottomBox.setStyle("-fx-border-color: black"); + + VBox addRemove = new VBox(); + addRemove.setSpacing(5); + Button add = new Button("ADD"); + Button remove = new Button("REMOVE"); + HBox indexContainer = new HBox(); + Label indexLabel = new Label("Index: "); + TextField indexTF = new TextField("0"); + indexTF.setMaxWidth(50); + indexContainer.getChildren().addAll(indexLabel, indexTF); + addRemove.getChildren().add(new Label("Add / Remove index")); + addRemove.getChildren().add(indexContainer); + addRemove.getChildren().add(add); + addRemove.getChildren().add(remove); + addRemove.getChildren().add(indexErr); + + + add.setOnMouseClicked(event -> { + try { + int index = Integer.parseInt(indexTF.getText()); + if (index >= 0 && index < tabPane.getTabs().size()) { + tabPane.getTabs().add(index, new Tab(index + "--- ")); + tabPane.requestFocus(); + } + indexErr.setText(""); + } catch (Exception e) { + indexErr.setText("Incorrect Index"); + } + }); + remove.setOnMouseClicked(event -> { + try { + int index = Integer.parseInt(indexTF.getText()); + if (index >= 0 && index < tabPane.getTabs().size()) { + tabPane.getTabs().remove(index); + tabPane.requestFocus(); + } + indexErr.setText(""); + } catch (Exception e) { + indexErr.setText("Incorrect Index"); + } + }); + + bottomBox.getChildren().add(addRemove); + + ToggleGroup side = new ToggleGroup(); + + ToggleButton top = new ToggleButton("TOP"); + top.setSelected(true); + top.setUserData(Side.TOP); + ToggleButton bottom = new ToggleButton("BOTTOM"); + bottom.setUserData(Side.BOTTOM); + ToggleButton left = new ToggleButton("LEFT"); + left.setUserData(Side.LEFT); + ToggleButton right = new ToggleButton("RIGHT"); + right.setUserData(Side.RIGHT); + + top.setToggleGroup(side); + bottom.setToggleGroup(side); + left.setToggleGroup(side); + right.setToggleGroup(side); + + side.selectedToggleProperty().addListener(observable -> { + if (side.getSelectedToggle() == null) { + tabPane.setSide(Side.TOP); + } else { + tabPane.setSide((Side) side.getSelectedToggle().getUserData()); + } + tabPane.requestFocus(); + }); + + VBox sides = new VBox(); + sides.setSpacing(5); + sides.getChildren().add(new Label("Sides")); + sides.getChildren().add(top); + sides.getChildren().add(bottom); + sides.getChildren().add(left); + sides.getChildren().add(right); + bottomBox.getChildren().add(sides); + + ToggleGroup dragPolicy = new ToggleGroup(); + dragPolicy.selectedToggleProperty().addListener(observable -> { + if (dragPolicy.getSelectedToggle() == null) { + tabPane.setTabDragPolicy(TabPane.TabDragPolicy.FIXED); + } else { + tabPane.setTabDragPolicy((TabPane.TabDragPolicy) dragPolicy.getSelectedToggle().getUserData()); + } + tabPane.requestFocus(); + }); + + ToggleButton reorder = new ToggleButton("REORDER"); + reorder.setUserData(TabPane.TabDragPolicy.REORDER); + reorder.setToggleGroup(dragPolicy); + ToggleButton fixed = new ToggleButton("FIXED"); + fixed.setSelected(true); + fixed.setUserData(TabPane.TabDragPolicy.FIXED); + fixed.setToggleGroup(dragPolicy); + + VBox dragPolicies = new VBox(); + dragPolicies.setSpacing(5); + dragPolicies.getChildren().add(new Label("Drag Policies")); + dragPolicies.getChildren().add(reorder); + dragPolicies.getChildren().add(fixed); + bottomBox.getChildren().add(dragPolicies); + + ToggleGroup orientation = new ToggleGroup(); + orientation.selectedToggleProperty().addListener(observable -> { + if (orientation.getSelectedToggle() == null) { + tabPane.setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT); + } else { + tabPane.setNodeOrientation((NodeOrientation) orientation.getSelectedToggle().getUserData()); + } + tabPane.requestFocus(); + }); + + ToggleButton ltr = new ToggleButton("LEFT TO RIGHT"); + ltr.setSelected(true); + ltr.setUserData(NodeOrientation.LEFT_TO_RIGHT); + ltr.setToggleGroup(orientation); + ToggleButton rtl = new ToggleButton("RIGHT TO LEFT"); + rtl.setUserData(NodeOrientation.RIGHT_TO_LEFT); + rtl.setToggleGroup(orientation); + VBox orientations = new VBox(); + orientations.setSpacing(5); + orientations.getChildren().add(new Label("Node Orientations")); + orientations.getChildren().add(ltr); + orientations.getChildren().add(rtl); + bottomBox.getChildren().add(orientations); + + HBox angleContainer = new HBox(); + Label angleLabel = new Label("Angle: "); + TextField angleTF = new TextField("0"); + angleTF.setMaxWidth(50); + angleContainer.getChildren().addAll(angleLabel, angleTF); + Label rotLabel = new Label("Rotate TabPane"); + Button rotate = new Button("ROTATE"); + rotate.setOnMouseClicked(event -> { + try { + tabPane.setRotate(Float.parseFloat(angleTF.getText())); + angleErr.setText(""); + } catch (Exception e) { + angleErr.setText("Incorrect Angle"); + } + tabPane.requestFocus(); + }); + VBox rotation = new VBox(); + rotation.setSpacing(5); + rotation.getChildren().addAll(rotLabel, angleContainer, rotate, angleErr); + bottomBox.getChildren().add(rotation); + + Label reverseLabel = new Label("Reverse order of tabs"); + Button reverse = new Button("REVERSE"); + reverse.setOnMouseClicked(event -> { + tabPane.getTabs().sort((o1, o2) -> { + if (tabPane.getTabs().indexOf(o1) > tabPane.getTabs().indexOf(o2)) { + return -1; + } else { + return 1; + } + }); + tabPane.requestFocus(); + }); + VBox revContainer = new VBox(); + revContainer.setSpacing(5); + revContainer.getChildren().addAll(reverseLabel, reverse); + bottomBox.getChildren().add(revContainer); + + bottomBox.setOnMousePressed(event -> { + angleErr.setText(""); + indexErr.setText(""); + }); + mainContent.getChildren().add(bottomBox); + return mainContent; + } + private void setupInternalTab() { StackPane internalTabContent = new StackPane(); --- old/modules/javafx.controls/src/main/java/javafx/scene/control/TabPane.java 2017-12-01 17:43:51.358819000 +0530 +++ new/modules/javafx.controls/src/main/java/javafx/scene/control/TabPane.java 2017-12-01 17:43:51.186819000 +0530 @@ -31,6 +31,7 @@ import java.util.Set; import com.sun.javafx.collections.UnmodifiableListSet; +import com.sun.javafx.scene.control.TabObservableList; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; @@ -38,7 +39,6 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.WritableValue; -import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.geometry.Side; @@ -151,7 +151,7 @@ } - private ObservableList tabs = FXCollections.observableArrayList(); + private ObservableList tabs = new TabObservableList<>(new ArrayList<>()); /** *

The tabs to display in this TabPane. Changing this ObservableList will @@ -848,4 +848,54 @@ */ UNAVAILABLE } + + + // TabDragPolicy // + private ObjectProperty tabDragPolicy; + + /** + * The drag policy for the tabs. The policy can be changed dynamically. + * + * @defaultValue TabDragPolicy.FIXED + * @return The tab drag policy property + * @since 10 + */ + public final ObjectProperty tabDragPolicyProperty() { + if (tabDragPolicy == null) { + tabDragPolicy = new SimpleObjectProperty(this, "tabDragPolicy", TabDragPolicy.FIXED); + } + return tabDragPolicy; + } + public final void setTabDragPolicy(TabDragPolicy value) { + tabDragPolicyProperty().set(value); + } + public final TabDragPolicy getTabDragPolicy() { + return tabDragPolicyProperty().get(); + } + + /** + * This enum specifies drag policies for tabs in a TabPane. + * + * @since 10 + */ + public enum TabDragPolicy { + /** + * The tabs remain fixed in their positions and cannot be dragged. + */ + FIXED, + + /** + * The tabs can be dragged to reorder them within the same TabPane. + * Users can perform the simple mouse press-drag-release gesture on a + * tab header to drag it to a new position. A tab can not be detached + * from its parent TabPane. + *

After a tab is reordered, the {@link #getTabs() tabs} list is + * permuted to reflect the updated order. + * A {@link javafx.collections.ListChangeListener.Change permutation + * change} event is fired to indicate which tabs were reordered. This + * reordering is done after the mouse button is released. While a tab + * is being dragged, the list of tabs is unchanged.

+ */ + REORDER + } } --- old/modules/javafx.controls/src/main/java/javafx/scene/control/skin/TabPaneSkin.java 2017-12-01 17:43:51.926819000 +0530 +++ new/modules/javafx.controls/src/main/java/javafx/scene/control/skin/TabPaneSkin.java 2017-12-01 17:43:51.742819000 +0530 @@ -27,12 +27,14 @@ import com.sun.javafx.scene.control.LambdaMultiplePropertyChangeListenerHandler; import com.sun.javafx.scene.control.Properties; +import com.sun.javafx.scene.control.TabObservableList; import com.sun.javafx.util.Utils; import javafx.animation.Animation; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; +import javafx.animation.Transition; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.WeakInvalidationListener; @@ -51,7 +53,10 @@ import javafx.css.StyleableProperty; import javafx.event.ActionEvent; import javafx.event.EventHandler; +import javafx.geometry.Bounds; import javafx.geometry.HPos; +import javafx.geometry.NodeOrientation; +import javafx.geometry.Point2D; import javafx.geometry.Pos; import javafx.geometry.Side; import javafx.geometry.VPos; @@ -68,6 +73,7 @@ import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.control.TabPane.TabClosingPolicy; +import javafx.scene.control.TabPane.TabDragPolicy; import javafx.scene.control.ToggleGroup; import javafx.scene.control.Tooltip; import javafx.scene.effect.DropShadow; @@ -91,8 +97,6 @@ import javafx.css.converter.EnumConverter; import com.sun.javafx.scene.control.behavior.TabPaneBehavior; -import com.sun.javafx.scene.traversal.Direction; -import com.sun.javafx.scene.traversal.TraversalEngine; import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString; @@ -576,39 +580,40 @@ while (c.next()) { if (c.wasPermutated()) { - TabPane tabPane = getSkinnable(); - List tabs = tabPane.getTabs(); + if (dragState != DragState.REORDER) { + TabPane tabPane = getSkinnable(); + List tabs = tabPane.getTabs(); + + // tabs sorted : create list of permutated tabs. + // clear selection, set tab animation to NONE + // remove permutated tabs, add them back in correct order. + // restore old selection, and old tab animation states. + int size = c.getTo() - c.getFrom(); + Tab selTab = tabPane.getSelectionModel().getSelectedItem(); + List permutatedTabs = new ArrayList(size); + getSkinnable().getSelectionModel().clearSelection(); + + // save and set tab animation to none - as it is not a good idea + // to animate on the same data for open and close. + TabAnimation prevOpenAnimation = openTabAnimation.get(); + TabAnimation prevCloseAnimation = closeTabAnimation.get(); + openTabAnimation.set(TabAnimation.NONE); + closeTabAnimation.set(TabAnimation.NONE); + for (int i = c.getFrom(); i < c.getTo(); i++) { + permutatedTabs.add(tabs.get(i)); + } - // tabs sorted : create list of permutated tabs. - // clear selection, set tab animation to NONE - // remove permutated tabs, add them back in correct order. - // restore old selection, and old tab animation states. - int size = c.getTo() - c.getFrom(); - Tab selTab = tabPane.getSelectionModel().getSelectedItem(); - List permutatedTabs = new ArrayList(size); - getSkinnable().getSelectionModel().clearSelection(); - - // save and set tab animation to none - as it is not a good idea - // to animate on the same data for open and close. - TabAnimation prevOpenAnimation = openTabAnimation.get(); - TabAnimation prevCloseAnimation = closeTabAnimation.get(); - openTabAnimation.set(TabAnimation.NONE); - closeTabAnimation.set(TabAnimation.NONE); - for (int i = c.getFrom(); i < c.getTo(); i++) { - permutatedTabs.add(tabs.get(i)); - } - - removeTabs(permutatedTabs); - addTabs(permutatedTabs, c.getFrom()); - openTabAnimation.set(prevOpenAnimation); - closeTabAnimation.set(prevCloseAnimation); - getSkinnable().getSelectionModel().select(selTab); + removeTabs(permutatedTabs); + addTabs(permutatedTabs, c.getFrom()); + openTabAnimation.set(prevOpenAnimation); + closeTabAnimation.set(prevCloseAnimation); + getSkinnable().getSelectionModel().select(selTab); + } } if (c.wasRemoved()) { tabsToRemove.addAll(c.getRemoved()); } - if (c.wasAdded()) { tabsToAdd.addAll(c.getAddedSubList()); insertPos = c.getFrom(); @@ -873,10 +878,16 @@ if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) { // build from the right tabX -= tabHeaderPrefWidth; - tabHeader.relocate(tabX, startY); + if (dragState != DragState.REORDER || + (tabHeader != dragTabHeader && tabHeader != dropAnimHeader)) { + tabHeader.relocate(tabX, startY); + } } else { // build from the left - tabHeader.relocate(tabX, startY); + if (dragState != DragState.REORDER || + (tabHeader != dragTabHeader && tabHeader != dropAnimHeader)) { + tabHeader.relocate(tabX, startY); + } tabX += tabHeaderPrefWidth; } } @@ -885,6 +896,7 @@ }; headersRegion.getStyleClass().setAll("headers-region"); headersRegion.setClip(headerClip); + setupReordering(headersRegion); headerBackground = new StackPane(); headerBackground.getStyleClass().setAll("tab-header-background"); @@ -1234,6 +1246,7 @@ setId(tab.getId()); setStyle(tab.getStyle()); setAccessibleRole(AccessibleRole.TAB_ITEM); + setViewOrder(1); this.tab = tab; clip = new Rectangle(); @@ -1915,4 +1928,347 @@ default: return super.queryAccessibleAttribute(attribute, parameters); } } -} + + // -------------------------- + // Tab Reordering + // -------------------------- + private enum DragState { + NONE, + START, + REORDER + } + private EventHandler headerDraggedHandler = this::handleHeaderDragged; + private EventHandler headerMousePressedHandler = this::handleHeaderMousePressed; + private EventHandler headerMouseReleasedHandler = this::handleHeaderMouseReleased; + + private int dragTabHeaderIndex; + private TabHeaderSkin dragTabHeader; + private TabHeaderSkin dropTabHeader; + private StackPane headersRegion; + private DragState dragState; + private int xLayoutDirection; + private Point2D dragEventStartLoc; + private Point2D dragEventPrevLoc; + private final static int Drag_LTR = 1; + private final static int Drag_RTL = -1; + private int prevDragDirection = Drag_LTR; + private final double DRAG_DIST_THRESHOLD = 0.75; + + // Reordering Animation + private static double ANIM_DURATION = 120; + private TabHeaderSkin dropAnimHeader; + private Tab swapTab = null; + private double dropHeaderSourceX; + private double dropHeaderTransitionX; + private final Animation dropHeaderAnim = new Transition() { + { + setInterpolator(Interpolator.EASE_BOTH); + setCycleDuration(Duration.millis(ANIM_DURATION)); + setOnFinished(event -> { + completeHeaderReordering(); + }); + } + protected void interpolate(double frac) { + dropAnimHeader.setLayoutX(dropHeaderSourceX + dropHeaderTransitionX * frac); + } + }; + private double dragHeaderStartX; + private double dragHeaderDestX; + private double dragHeaderSourceX; + private double dragHeaderTransitionX; + private final Animation dragHeaderAnim = new Transition() { + { + setInterpolator(Interpolator.EASE_OUT); + setCycleDuration(Duration.millis(ANIM_DURATION)); + setOnFinished(event -> { + resetDrag(); + }); + } + protected void interpolate(double frac) { + dragTabHeader.setLayoutX(dragHeaderSourceX + dragHeaderTransitionX * frac); + } + }; + + // Helper methods for managing the listeners based on TabDragPolicy. + private void addReorderListeners(Node n) { + n.addEventHandler(MouseEvent.MOUSE_PRESSED, headerMousePressedHandler); + n.addEventHandler(MouseEvent.MOUSE_RELEASED, headerMouseReleasedHandler); + n.addEventHandler(MouseEvent.MOUSE_DRAGGED, headerDraggedHandler); + } + + private void removeReorderListeners(Node n) { + n.removeEventHandler(MouseEvent.MOUSE_PRESSED, headerMousePressedHandler); + n.removeEventHandler(MouseEvent.MOUSE_RELEASED, headerMouseReleasedHandler); + n.removeEventHandler(MouseEvent.MOUSE_DRAGGED, headerDraggedHandler); + } + + private ListChangeListener childListener = new ListChangeListener() { + public void onChanged(Change change) { + while (change.next()) { + if (change.wasAdded()) { + for(Node n : change.getAddedSubList()) { + addReorderListeners(n); + } + } + if (change.wasRemoved()) { + for(Node n : change.getRemoved()) { + removeReorderListeners(n); + } + } + } + } + }; + + private void updateListeners() { + if (getSkinnable().getTabDragPolicy() == TabDragPolicy.FIXED || + getSkinnable().getTabDragPolicy() == null) { + for (Node n : headersRegion.getChildren()) { + removeReorderListeners(n); + } + headersRegion.getChildren().removeListener(childListener); + } else if (getSkinnable().getTabDragPolicy() == TabDragPolicy.REORDER) { + for (Node n : headersRegion.getChildren()) { + addReorderListeners(n); + } + headersRegion.getChildren().addListener(childListener); + } + } + + private void setupReordering(StackPane headerRegion) { + dragState = DragState.NONE; + headersRegion = headerRegion; + updateListeners(); + getSkinnable().tabDragPolicyProperty().addListener((observable, oldValue, newValue) -> { + if (oldValue != newValue) { + updateListeners(); + } + }); + } + + private void handleHeaderMousePressed(MouseEvent event) { + startDrag(event); + } + + private void handleHeaderMouseReleased(MouseEvent event) { + stopDrag(); + event.consume(); + } + + private void handleHeaderDragged(MouseEvent event) { + perfromDrag(event); + } + + private Point2D rotate(Point2D pt) { + double angle = getSkinnable().getRotate(); + if (angle == 0) { + return pt; + } + // Rotate the point pt by -(rotation angle of TabPane) with + // respect to 0,0,1 axis passing through dragEventStartLoc, + // and return the rotated point. + double x1 = pt.getX() - dragEventStartLoc.getX(); + double y1 = pt.getY() - dragEventStartLoc.getY(); + double x = x1 * Math.cos(Math.toRadians(angle)) + y1 * Math.sin(Math.toRadians(angle)); + double y = y1 * Math.cos(Math.toRadians(angle)) - x1 * Math.sin(Math.toRadians(angle)); + x += dragEventStartLoc.getX(); + y += dragEventStartLoc.getY(); + return new Point2D(x , y); + } + + private double getDragDelta(Point2D curr, Point2D prev) { + if (getSkinnable().getSide().equals(Side.LEFT) || + getSkinnable().getSide().equals(Side.RIGHT)) { + return curr.getY() - prev.getY(); + } + return curr.getX() - prev.getX(); + } + + private int deriveTabHeaderLayoutXDirection() { + if (getSkinnable().getSide().equals(Side.TOP) || + getSkinnable().getSide().equals(Side.RIGHT)) { + // TabHeaderSkin are laid out in left to right direction + return Drag_LTR; + } + // TabHeaderSkin are laid out in right to left direction + return Drag_RTL; + } + + private void perfromDrag(MouseEvent event) { + int dragDirection; + double dragHeaderNewLayoutX; + Bounds dragHeaderBounds; + Bounds dropHeaderBounds; + double draggedDist; + Point2D mouseCurrentLoc = rotate(new Point2D(event.getScreenX(), event.getScreenY())); + double dragDelta = getDragDelta(mouseCurrentLoc, dragEventPrevLoc); + if (getSkinnable().getNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) { + if (getSkinnable().getSide().equals(Side.TOP) || + getSkinnable().getSide().equals(Side.BOTTOM)) + dragDelta = -dragDelta; + } + + // Stop dropHeaderAnim if direction of drag is changed + if (dragDelta > 0) { + dragDirection = Drag_LTR; + } else { + dragDirection = Drag_RTL; + } + if (prevDragDirection != dragDirection) { + stopAnim(dropHeaderAnim); + prevDragDirection = dragDirection; + } + + dragHeaderNewLayoutX = dragTabHeader.getLayoutX() + xLayoutDirection * dragDelta; + + if (dragHeaderNewLayoutX >= 0 && + dragHeaderNewLayoutX + dragTabHeader.getWidth() <= headersRegion.getWidth()) { + + dragState = DragState.REORDER; + dragTabHeader.setLayoutX(dragHeaderNewLayoutX); + dragHeaderBounds = dragTabHeader.getBoundsInParent(); + + if (dragDirection == Drag_LTR) { + // Dragging the tab header towards right + // Last tab header can not be dragged towards right. + // When the mouse is moved too fast, sufficient number of events + // are not generated. Hence it is required to check all possible + // headers to be reordered. + for (int i = dragTabHeaderIndex + 1; i < headersRegion.getChildren().size(); i++) { + dropTabHeader = (TabHeaderSkin) headersRegion.getChildren().get(i); + + // Check if the tab header is already reordering. + if (dropAnimHeader != dropTabHeader) { + dropHeaderBounds = dropTabHeader.getBoundsInParent(); + + if (xLayoutDirection == Drag_LTR) { + draggedDist = dragHeaderBounds.getMaxX() - dropHeaderBounds.getMinX(); + } else { + draggedDist = dropHeaderBounds.getMaxX() - dragHeaderBounds.getMinX(); + } + + // A tab is reordered when dragged tab corsses DRAG_DIST_THRESHOLD% of next tabs width. + if (draggedDist > dropHeaderBounds.getWidth() * DRAG_DIST_THRESHOLD) { + stopAnim(dropHeaderAnim); + // Distance by which tab header should be animated in X. + dropHeaderTransitionX = xLayoutDirection * -dragHeaderBounds.getWidth(); + if (xLayoutDirection == Drag_LTR) { + dragHeaderDestX = dropHeaderBounds.getMaxX() - dragHeaderBounds.getWidth(); + } else { + dragHeaderDestX = dropHeaderBounds.getMinX(); + } + startHeaderReorderingAnim(); + } else { + break; + } + } + } + } else { + // Dragging the tab header towards left + // First tab header can not be dragged towards left. + // When the mouse is moved too fast, sufficient number of events + // are not generated. Hence it is required to check all possible + // headers to be reordered. + for (int i = dragTabHeaderIndex - 1; i >= 0; i--) { + dropTabHeader = (TabHeaderSkin) headersRegion.getChildren().get(i); + + // Check if the tab header is already reordering. + if (dropAnimHeader != dropTabHeader) { + dropHeaderBounds = dropTabHeader.getBoundsInParent(); + + if (xLayoutDirection == Drag_LTR) { + draggedDist = dropHeaderBounds.getMaxX() - dragHeaderBounds.getMinX(); + } else { + draggedDist = dragHeaderBounds.getMaxX() - dropHeaderBounds.getMinX(); + } + + // A tab is reordered when dragged tab crosses DRAG_DIST_THRESHOLD% of next tabs width. + if (draggedDist > dropHeaderBounds.getWidth() * DRAG_DIST_THRESHOLD) { + stopAnim(dropHeaderAnim); + // Distance by which tab header should be animated in X position. + dropHeaderTransitionX = xLayoutDirection * dragHeaderBounds.getWidth(); + if (xLayoutDirection == Drag_LTR) { + dragHeaderDestX = dropHeaderBounds.getMinX(); + } else { + dragHeaderDestX = dropHeaderBounds.getMaxX() - dragHeaderBounds.getWidth(); + } + startHeaderReorderingAnim(); + } else { + break; + } + } + } + } + } + dragEventPrevLoc = mouseCurrentLoc; + event.consume(); + } + + private void startDrag(MouseEvent event) { + stopAnim(dropHeaderAnim); + stopAnim(dragHeaderAnim); + dragTabHeader = (TabHeaderSkin) event.getSource(); + if (dragTabHeader != null) { + dragState = DragState.START; + swapTab = null; + xLayoutDirection = deriveTabHeaderLayoutXDirection(); + dragEventStartLoc = new Point2D(event.getScreenX(), event.getScreenY()); + dragEventPrevLoc = new Point2D(event.getScreenX(), event.getScreenY()); + dragTabHeaderIndex = headersRegion.getChildren().indexOf(dragTabHeader); + dragTabHeader.setViewOrder(0); + dragHeaderStartX = dragHeaderDestX = dragTabHeader.getLayoutX(); + } + } + + private void stopDrag() { + if (dragState == DragState.START) { + // No drag action was performed. + resetDrag(); + return; + } + // Animate tab header being dragged to its final position. + dragHeaderSourceX = dragTabHeader.getLayoutX(); + dragHeaderTransitionX = dragHeaderDestX - dragHeaderSourceX; + dragHeaderAnim.playFromStart(); + + // Reorder the tab list + if (dragHeaderStartX != dragHeaderDestX) { + ((TabObservableList) getSkinnable().getTabs()).reorder(dragTabHeader.tab, swapTab); + swapTab = null; + } + } + + private void resetDrag() { + dragState = DragState.NONE; + dragTabHeader.setViewOrder(1); + dragTabHeader = null; + dropTabHeader = null; + headersRegion.requestLayout(); + } + + // Animate tab header being dropped-on to its new position. + private void startHeaderReorderingAnim() { + dropAnimHeader = dropTabHeader; + swapTab = dropAnimHeader.tab; + dropHeaderSourceX = dropAnimHeader.getLayoutX(); + dropHeaderAnim.playFromStart(); + } + + // Remove dropAnimHeader and add at the index position of dragTabHeader. + private void completeHeaderReordering() { + if (dropAnimHeader != null) { + headersRegion.getChildren().remove(dropAnimHeader); + headersRegion.getChildren().add(dragTabHeaderIndex, dropAnimHeader); + dropAnimHeader = null; + headersRegion.requestLayout(); + dragTabHeaderIndex = headersRegion.getChildren().indexOf(dragTabHeader); + } + } + + // Helper method to stop an animation. + private void stopAnim(Animation anim) { + if (anim.getStatus() == Animation.Status.RUNNING) { + anim.getOnFinished().handle(null); + anim.stop(); + } + } +} \ No newline at end of file --- old/modules/javafx.controls/src/test/java/test/javafx/scene/control/TabPaneTest.java 2017-12-01 17:43:52.542819000 +0530 +++ new/modules/javafx.controls/src/test/java/test/javafx/scene/control/TabPaneTest.java 2017-12-01 17:43:52.362819000 +0530 @@ -442,6 +442,22 @@ assertSame(tabPane.getTabClosingPolicy(), TabPane.TabClosingPolicy.UNAVAILABLE); } + @Test public void setTabDragPolicyAndSeeValueIsReflectedInModel() { + tabPane.setTabDragPolicy(TabPane.TabDragPolicy.REORDER); + assertSame(tabPane.tabDragPolicyProperty().getValue(), TabPane.TabDragPolicy.REORDER); + + tabPane.setTabDragPolicy(TabPane.TabDragPolicy.FIXED); + assertSame(tabPane.tabDragPolicyProperty().getValue(), TabPane.TabDragPolicy.FIXED); + } + + @Test public void setTabDragPolicyAndSeeValue() { + tabPane.setTabDragPolicy(TabPane.TabDragPolicy.REORDER); + assertSame(tabPane.getTabDragPolicy(), TabPane.TabDragPolicy.REORDER); + + tabPane.setTabDragPolicy(TabPane.TabDragPolicy.FIXED); + assertSame(tabPane.getTabDragPolicy(), TabPane.TabDragPolicy.FIXED); + } + @Test public void setRotateGraphicAndSeeValueIsReflectedInModel() { tabPane.setRotateGraphic(true); assertTrue(tabPane.rotateGraphicProperty().getValue()); --- /dev/null 2017-12-01 14:09:20.136665000 +0530 +++ new/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/TabObservableList.java 2017-12-01 17:43:52.958819000 +0530 @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.sun.javafx.scene.control; + +import com.sun.javafx.collections.NonIterableChange; +import com.sun.javafx.collections.ObservableListWrapper; +import javafx.scene.control.Tab; +import java.util.List; +import java.util.ListIterator; + +public class TabObservableList extends ObservableListWrapper { + private final List tabList; + + public TabObservableList(List list) { + super(list); + tabList = list; + } + + public void reorder(Tab fromTab, Tab toTab) { + if (!tabList.contains(fromTab) || !tabList.contains(toTab) || fromTab == toTab) { + return; + } + // Perform reorder with the array of tabs. + Object[] a = tabList.toArray(); + int fromIndex = tabList.indexOf(fromTab); + int toIndex = tabList.indexOf(toTab); + if (fromIndex == -1 || toIndex == -1) { + return; + } + int direction = (toIndex - fromIndex) / Math.abs(toIndex - fromIndex); + + for (int j = fromIndex; j != toIndex; j += direction) { + a[j] = a[j + direction]; + } + a[toIndex] = fromTab; + + // Update the list with reordered array. + ListIterator iter = tabList.listIterator(); + for (int j = 0; j < tabList.size(); j++) { + iter.next(); + iter.set(a[j]); + } + + // Update selected tab & index. + fromTab.getTabPane().getSelectionModel().select(fromTab); + + // Fire permutation change event. + int permSize = Math.abs(toIndex - fromIndex) + 1; + int[] perm = new int[permSize]; + int from = direction > 0 ? fromIndex : toIndex; + int to = direction < 0 ? fromIndex : toIndex; + for (int i = 0; i < permSize; ++i) { + perm[i] = i + from; + } + fireChange(new NonIterableChange.SimplePermutationChange(from, to + 1, perm, this)); + } +} --- /dev/null 2017-12-01 14:09:20.136665000 +0530 +++ new/tests/system/src/test/java/test/robot/javafx/scene/TabPaneDragPolicyTest.java 2017-12-01 17:43:53.490819000 +0530 @@ -0,0 +1,383 @@ +/* + * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package test.robot.javafx.scene; + +import com.sun.glass.ui.Robot; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.collections.ListChangeListener; +import javafx.scene.Scene; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.stage.WindowEvent; +import javafx.geometry.Side; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.fail; + +public class TabPaneDragPolicyTest { + static CountDownLatch startupLatch; + static CountDownLatch[] latches = new CountDownLatch[4]; + static volatile Stage stage; + static volatile Scene scene; + static Robot robot; + static TabPane tabPane; + static Tab[] tabs = new Tab[4]; + static Tab expectedTab; + static Tab selectedTab; + static int sceneWidth = 250; + static int sceneHeight = sceneWidth; + final int DRAG_DISTANCE = sceneWidth - 50; + final int DX = 15; + final int DY = DX; + String permutedSeq = "tab1tab2tab3tab0"; + String reorderSeq = "tab1tab2tab3tab0"; + boolean testResult = false; + CountDownLatch listenerLatch = new CountDownLatch(1); + + class ReorderChangeListener implements ListChangeListener { + @Override + public void onChanged(Change c) { + while (c.next()) { + if (c.wasPermutated()) { + String list = ""; + for (int i = c.getFrom(); i < c.getTo(); i++) { + list += tabPane.getTabs().get(i).getText(); + } + testResult = permutedSeq.equals(list); + list = ""; + for (Tab t : tabPane.getTabs()) { + list += t.getText(); + } + testResult = testResult && reorderSeq.equals(list); + listenerLatch.countDown(); + } + } + }; + } + + class FixedChangeListener implements ListChangeListener { + @Override + public void onChanged(Change c) { + testResult = false; + listenerLatch.countDown(); + }; + } + + ReorderChangeListener reorderListener = new ReorderChangeListener(); + FixedChangeListener fixedListener = new FixedChangeListener(); + + public static void main(String[] args) { + initFX(); + TabPaneDragPolicyTest test = new TabPaneDragPolicyTest(); + + test.testReorderTop(); + test.testReorderBottom(); + test.testReorderLeft(); + test.testReorderRight(); + + test.testFixedTop(); + test.testFixedBottom(); + test.testFixedLeft(); + test.testFixedRight(); + + exit(); + } + + @Test + public void testReorderTop() { + expectedTab = tabs[1]; + testResult = false; + tabPane.setTabDragPolicy(TabPane.TabDragPolicy.REORDER); + tabPane.setSide(Side.TOP); + setupTabPane(); + tabPane.getTabs().addListener(reorderListener); + testReorder(DX, DY, 1, 0, false); + tabPane.getTabs().removeListener(reorderListener); + selectedTab = (Tab)tabPane.getSelectionModel().getSelectedItem(); + Assert.assertEquals("Expected " + expectedTab.getText() + " to be " + + "first tab after reordering.", expectedTab.getText(), selectedTab.getText()); + Assert.assertTrue("Incorrect permutation change received", testResult); + } + + @Test + public void testReorderBottom() { + expectedTab = tabs[1]; + testResult = false; + tabPane.setTabDragPolicy(TabPane.TabDragPolicy.REORDER); + tabPane.setSide(Side.BOTTOM); + setupTabPane(); + tabPane.getTabs().addListener(reorderListener); + testReorder(DX, sceneHeight - DY, 1, 0, false); + tabPane.getTabs().removeListener(reorderListener); + selectedTab = (Tab)tabPane.getSelectionModel().getSelectedItem(); + Assert.assertEquals("Expected " + expectedTab.getText() + " to be " + + "first tab after reordering.", expectedTab.getText(), selectedTab.getText()); + Assert.assertTrue("Incorrect permutation change received", testResult); + } + + @Test + public void testReorderLeft() { + expectedTab = tabs[1]; + testResult = false; + tabPane.setTabDragPolicy(TabPane.TabDragPolicy.REORDER); + tabPane.setSide(Side.LEFT); + setupTabPane(); + tabPane.getTabs().addListener(reorderListener); + testReorder(DX, DY, 0, 1, false); + tabPane.getTabs().removeListener(reorderListener); + selectedTab = (Tab)tabPane.getSelectionModel().getSelectedItem(); + Assert.assertEquals("Expected " + expectedTab.getText() + " to be " + + "first tab after reordering.", expectedTab.getText(), selectedTab.getText()); + Assert.assertTrue("Incorrect permutation change received", testResult); + } + + @Test + public void testReorderRight() { + expectedTab = tabs[1]; + testResult = false; + tabPane.setTabDragPolicy(TabPane.TabDragPolicy.REORDER); + tabPane.setSide(Side.RIGHT); + setupTabPane(); + tabPane.getTabs().addListener(reorderListener); + testReorder(sceneWidth - DX, DY, 0, 1, false); + tabPane.getTabs().removeListener(reorderListener); + selectedTab = (Tab)tabPane.getSelectionModel().getSelectedItem(); + Assert.assertEquals("Expected " + expectedTab.getText() + " to be " + + "first tab after reordering.", expectedTab.getText(), selectedTab.getText()); + Assert.assertTrue("Incorrect permutation change received", testResult); + } + + @Test + public void testFixedTop() { + expectedTab = tabs[0]; + testResult = true; + tabPane.setTabDragPolicy(TabPane.TabDragPolicy.FIXED); + tabPane.setSide(Side.TOP); + setupTabPane(); + tabPane.getTabs().addListener(fixedListener); + testReorder(DX, DY, 1, 0, true); + tabPane.getTabs().removeListener(fixedListener); + selectedTab = (Tab)tabPane.getSelectionModel().getSelectedItem(); + Assert.assertEquals("Expected " + expectedTab.getText() + " to remain " + + "first tab.", expectedTab.getText(), selectedTab.getText()); + Assert.assertTrue("Change event should not be received", testResult); + } + + @Test + public void testFixedBottom() { + expectedTab = tabs[0]; + testResult = true; + tabPane.setTabDragPolicy(TabPane.TabDragPolicy.FIXED); + tabPane.setSide(Side.BOTTOM); + setupTabPane(); + tabPane.getTabs().addListener(fixedListener); + testReorder(DX, sceneHeight - DY, 1, 0, true); + tabPane.getTabs().removeListener(fixedListener); + selectedTab = (Tab)tabPane.getSelectionModel().getSelectedItem(); + Assert.assertEquals("Expected " + expectedTab.getText() + " to remain " + + "first tab.", expectedTab.getText(), selectedTab.getText()); + Assert.assertTrue("Change event should not be received", testResult); + } + + @Test + public void testFixedLeft() { + expectedTab = tabs[0]; + testResult = true; + tabPane.setTabDragPolicy(TabPane.TabDragPolicy.FIXED); + tabPane.setSide(Side.LEFT); + setupTabPane(); + tabPane.getTabs().addListener(fixedListener); + testReorder(DX, DY, 0, 1, true); + tabPane.getTabs().removeListener(fixedListener); + selectedTab = (Tab)tabPane.getSelectionModel().getSelectedItem(); + Assert.assertEquals("Expected " + expectedTab.getText() + " to remain " + + "first tab.", expectedTab.getText(), selectedTab.getText()); + Assert.assertTrue("Change event should not be received", testResult); + } + + @Test + public void testFixedRight() { + expectedTab = tabs[0]; + testResult = true; + tabPane.setTabDragPolicy(TabPane.TabDragPolicy.FIXED); + tabPane.setSide(Side.RIGHT); + setupTabPane(); + tabPane.getTabs().addListener(fixedListener); + testReorder(sceneWidth - DX, DY, 0, 1, true); + tabPane.getTabs().removeListener(fixedListener); + selectedTab = (Tab)tabPane.getSelectionModel().getSelectedItem(); + Assert.assertEquals("Expected " + expectedTab.getText() + " to remain " + + "first tab.", expectedTab.getText(), selectedTab.getText()); + Assert.assertTrue("Change event should not be received", testResult); + } + + public void testReorder(int dX, int dY, int xIncr, int yIncr, boolean isFixed) { + try { + Thread.sleep(1000); // Wait for tabPane to layout + } catch (Exception ex) { + System.out.println("Thread was interrupted." + ex); + } + listenerLatch = new CountDownLatch(1); + Platform.runLater(() -> { + robot.mouseMove((int)(scene.getWindow().getX() + scene.getX() + dX), (int)(scene.getWindow().getY() + scene.getY() + dY)); + robot.mousePress(Robot.MOUSE_LEFT_BTN); + robot.mouseRelease(Robot.MOUSE_LEFT_BTN); + }); + waitForLatch(latches[0], 5, "Timeout waiting tabs[0] to get selected."); + + CountDownLatch pressLatch = new CountDownLatch(1); + Platform.runLater(() -> { + robot.mousePress(Robot.MOUSE_LEFT_BTN); + pressLatch.countDown(); + }); + waitForLatch(pressLatch, 5, "Timeout waiting for robot.mousePress(Robot.MOUSE_LEFT_BTN)."); + for (int i = 0; i < DRAG_DISTANCE; i++) { + final int c = i; + CountDownLatch moveLatch = new CountDownLatch(1); + Platform.runLater(() -> { + if (xIncr > 0) { + // Top & Bottom + robot.mouseMove((int)(scene.getWindow().getX() + scene.getX() + dX) + c, (int)(scene.getWindow().getY() + scene.getY() + dY)); + } else { + // Left & Right + robot.mouseMove((int)(scene.getWindow().getX() + scene.getX() + dX), (int)(scene.getWindow().getY() + scene.getY() + dY) + c); + } + moveLatch.countDown(); + }); + waitForLatch(moveLatch, 5, "Timeout waiting for robot.mouseMove(023)."); + } + + CountDownLatch releaseLatch = new CountDownLatch(1); + Platform.runLater(() -> { + robot.mouseRelease(Robot.MOUSE_LEFT_BTN); + releaseLatch.countDown(); + }); + waitForLatch(releaseLatch, 5, "Timeout waiting for robot.mouseRelease(Robot.MOUSE_LEFT_BTN)."); + + if (isFixed) { + tabPane.getSelectionModel().select(tabs[2]); + waitForLatch(latches[2], 5, "Timeout waiting tabs[2] to get selected."); + latches[0] = new CountDownLatch(1); + } + + Platform.runLater(() -> { + robot.mouseMove((int)(scene.getWindow().getX() + scene.getX() + dX), (int)(scene.getWindow().getY() + scene.getY() + dY)); + robot.mousePress(Robot.MOUSE_LEFT_BTN); + robot.mouseRelease(Robot.MOUSE_LEFT_BTN); + }); + + if (isFixed) { + // For FIXED drag policy, tabs[0] should remain the first tab. + waitForLatch(listenerLatch, 1, "Timeout waiting ChangeListener to get called."); + waitForLatch(latches[0], 5, "Timeout waiting tabs[0] to get selected."); + } else { + // For REORDER drag policy, tabs[1] should be the first tab. + waitForLatch(listenerLatch, 5, "Timeout waiting ChangeListener to get called."); + waitForLatch(latches[1], 5, "Timeout waiting tabs[1] to get selected."); + } + } + + @After + public void teardown() { + CountDownLatch latch = new CountDownLatch(1); + Platform.runLater(() -> { + tabPane.getTabs().removeAll(tabs); + latch.countDown(); + }); + waitForLatch(latch, 5, "Timeout waiting for TabPane tearDown."); + } + + @AfterClass + public static void exit() { + Platform.runLater(() -> { + stage.hide(); + }); + Platform.exit(); + } + + public static void waitForLatch(CountDownLatch latch, int seconds, String msg) { + try { + if (!latch.await(seconds, TimeUnit.SECONDS)) { + System.out.println(msg); + } + } catch (Exception ex) { + System.out.println("Unexpected exception: " + ex); + } + } + + public static class TestApp extends Application { + @Override + public void start(Stage primaryStage) { + robot = com.sun.glass.ui.Application.GetApplication().createRobot(); + stage = primaryStage; + for (int i = 0 ; i < 4; ++i) { + tabs[i] = new Tab("tab" + i); + } + tabPane = new TabPane(); + tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + scene = new Scene(tabPane, sceneWidth, sceneHeight); + stage.setScene(scene); + stage.initStyle(StageStyle.UNDECORATED); + stage.addEventHandler(WindowEvent.WINDOW_SHOWN, e -> + Platform.runLater(startupLatch::countDown)); + stage.setAlwaysOnTop(true); + stage.show(); + } + } + + public static void setupTabPane() { + CountDownLatch latch = new CountDownLatch(1); + Platform.runLater(() -> { + tabPane.getTabs().addAll(tabs); + tabPane.getSelectionModel().select(tabs[2]); + for (int i = 0 ; i < 4; ++i) { + final int c = i; + latches[i] = new CountDownLatch(1); + tabs[i].setOnSelectionChanged(event -> { + latches[c].countDown(); + }); + } + latch.countDown(); + }); + waitForLatch(latch, 5, "Timeout waiting for TabPane setup."); + } + + @BeforeClass + public static void initFX() { + startupLatch = new CountDownLatch(1); + new Thread(() -> Application.launch(TestApp.class, (String[])null)).start(); + waitForLatch(startupLatch, 10, "Timeout waiting for FX runtime to start"); + } +}