changeset 10736:efbe2743f401

8187074: [TabPane] Support reordering of Tabs within a TabPane Reviewed-by: kcr, aghaisas
author arapte
date Sat, 09 Dec 2017 00:42:02 +0530
parents f9706c9613a5
children cf93d081a173
files apps/toys/Hello/src/main/java/hello/HelloTabPane.java modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/TabObservableList.java modules/javafx.controls/src/main/java/javafx/scene/control/TabPane.java modules/javafx.controls/src/main/java/javafx/scene/control/skin/TabPaneSkin.java modules/javafx.controls/src/test/java/test/javafx/scene/control/TabPaneTest.java tests/system/src/test/java/test/robot/javafx/scene/TabPaneDragPolicyTest.java
diffstat 6 files changed, 1283 insertions(+), 33 deletions(-) [+]
line wrap: on
line diff
--- a/apps/toys/Hello/src/main/java/hello/HelloTabPane.java	Thu Dec 07 18:58:31 2017 -0800
+++ b/apps/toys/Hello/src/main/java/hello/HelloTabPane.java	Sat Dec 09 00:42:02 2017 +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,9 +88,10 @@
         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);
+        final Scene scene = new Scene(new Group(), 1200, 800);
         scene.setFill(Color.GHOSTWHITE);
 
 
@@ -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,336 @@
         stage.show();
     }
 
+    private VBox setupDragPolicyTab() {
+        Label indexErr = new Label("");
+        Label angleErr = new Label("");
+        final int LABEL_WIDTH = 150;
+        VBox mainContent = new VBox();
+        mainContent.setSpacing(12);
+        mainContent.setMinSize(1000, 400);
+
+        TabPane tabPane = new TabPane();
+        tabPane.setMinSize(500, 400);
+        tabPane.setMaxSize(500, 400);
+        tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.ALL_TABS);
+        HBox tabPaneAndInstr = new HBox();
+        HBox tabPaneParent = new HBox();
+        tabPaneParent.getChildren().add(tabPane);
+        tabPaneAndInstr.getChildren().add(tabPaneParent);
+
+        for (int i = 0; i < 7; ++i) {
+            String text = "" + i + i + i;
+            HBox hb = new HBox();
+            Tab tab = new Tab(text);
+            TextArea ta = new TextArea(text);
+            ta.setMaxSize(300, 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."
+        );
+        tabPaneAndInstr.getChildren().add(instructions);
+        mainContent.getChildren().add(tabPaneAndInstr);
+
+        Label permuted = new Label("Permuted tabs");
+        permuted.setMinWidth(LABEL_WIDTH);
+        Label outputPermutedTabs = new Label();
+        Label added = new Label   ("Added tabs");
+        added.setMinWidth(LABEL_WIDTH);
+        Label outputAddedTabs = new Label();
+        Label removed = new Label ("Removed tabs");
+        removed.setMinWidth(LABEL_WIDTH);
+        Label outputRemovedTabs = new Label();
+        Label getTabs = new Label ("Current order of Tabs");
+        getTabs.setMinWidth(LABEL_WIDTH);
+        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(10);
+        notifications.setStyle("-fx-border-color: black");
+        notifications.setPadding(new Insets(10));
+        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<Tab>) 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 (!list.equals("")) {
+                    list = list.substring(0, list.length() - 3);
+                }
+
+                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() + ",  ";
+                    }
+                    if (!list.equals("")) {
+                        list = list.substring(0, list.length() - 3);
+                    }
+                    outputRemovedTabs.setText(list);
+                }
+                list = "";
+                for (Tab t : tabPane.getTabs()) {
+                    list += t.getText() + ",  ";
+                }
+                if (!list.equals("")) {
+                    list = list.substring(0, list.length() - 3);
+                }
+                outputListOfTabs.setText(list);
+            }
+        });
+
+        HBox actions = new HBox();
+        actions.setStyle("-fx-border-color: black");
+        actions.setSpacing(20);
+        actions.setPadding(new Insets(10));
+
+        VBox sideDragPolNodeOri = new VBox();
+        sideDragPolNodeOri.setSpacing(5);
+        actions.getChildren().add(sideDragPolNodeOri);
+
+        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);
+                top.setSelected(true);
+            } else {
+                tabPane.setSide((Side) side.getSelectedToggle().getUserData());
+            }
+            tabPane.requestFocus();
+        });
+
+        HBox sides = new HBox();
+        sides.setSpacing(5);
+        Label sid = new Label("Sides");
+        sid.setMinWidth(LABEL_WIDTH);
+        sides.getChildren().add(sid);
+        sides.getChildren().add(top);
+        sides.getChildren().add(bottom);
+        sides.getChildren().add(left);
+        sides.getChildren().add(right);
+        sideDragPolNodeOri.getChildren().add(sides);
+
+        ToggleGroup dragPolicy = new ToggleGroup();
+        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);
+
+        HBox dragPolicies = new HBox();
+        dragPolicies.setSpacing(5);
+        Label dp = new Label("Drag Policies");
+        dp.setMinWidth(LABEL_WIDTH);
+        dragPolicies.getChildren().add(dp);
+        dragPolicies.getChildren().add(fixed);
+        dragPolicies.getChildren().add(reorder);
+        sideDragPolNodeOri.getChildren().add(dragPolicies);
+        dragPolicy.selectedToggleProperty().addListener(observable -> {
+            if (dragPolicy.getSelectedToggle() == null) {
+                fixed.setSelected(true);
+                tabPane.setTabDragPolicy(TabPane.TabDragPolicy.FIXED);
+            } else {
+                tabPane.setTabDragPolicy((TabPane.TabDragPolicy) dragPolicy.getSelectedToggle().getUserData());
+            }
+            tabPane.requestFocus();
+        });
+
+        ToggleGroup orientation = new ToggleGroup();
+        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);
+        HBox orientations = new HBox();
+        orientations.setSpacing(5);
+        Label no = new Label("Node Orientations");
+        no.setMinWidth(LABEL_WIDTH);
+        orientations.getChildren().add(no);
+        orientations.getChildren().add(ltr);
+        orientations.getChildren().add(rtl);
+        sideDragPolNodeOri.getChildren().add(orientations);
+        orientation.selectedToggleProperty().addListener(observable -> {
+            if (orientation.getSelectedToggle() == null) {
+                tabPane.setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT);
+                ltr.setSelected(true);
+            } else {
+                tabPane.setNodeOrientation((NodeOrientation) orientation.getSelectedToggle().getUserData());
+            }
+            tabPane.requestFocus();
+        });
+
+        VBox addRemRotRev = new VBox();
+        addRemRotRev.setSpacing(5);
+        actions.getChildren().add(addRemRotRev);
+
+        HBox addRemove = new HBox();
+        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(70);
+        indexContainer.getChildren().addAll(indexLabel, indexTF);
+        Label ar = new Label("Add / Remove index");
+        ar.setMinWidth(LABEL_WIDTH);
+        addRemove.getChildren().add(ar);
+        addRemove.getChildren().add(add);
+        addRemove.getChildren().add(remove);
+        addRemove.getChildren().add(indexContainer);
+        addRemove.getChildren().add(indexErr);
+
+        add.setOnMouseClicked(event -> {
+            try {
+                int i = Integer.parseInt(indexTF.getText());
+                if (i >= 0 && i < tabPane.getTabs().size()) {
+                    tabPane.getTabs().add(i, new Tab("" + i + i + i + i));
+                    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");
+            }
+        });
+        addRemRotRev.getChildren().add(addRemove);
+
+        HBox angleContainer = new HBox();
+        Label angleLabel = new Label("Angle: ");
+        TextField angleTF = new TextField("0");
+        angleTF.setMaxWidth(70);
+        angleContainer.getChildren().addAll(angleLabel, angleTF);
+        Label rotLabel = new Label("Rotate");
+        rotLabel.setMinWidth(LABEL_WIDTH);
+        Button rotate = new Button("TabPane");
+        rotate.setOnMouseClicked(event -> {
+            try {
+                tabPane.setRotate(Float.parseFloat(angleTF.getText()));
+                angleErr.setText("");
+            } catch (Exception e) {
+                angleErr.setText("Incorrect Angle");
+            }
+            tabPane.requestFocus();
+        });
+        Button rotateParent = new Button("TabPane Parent");
+        rotateParent.setOnMouseClicked(event -> {
+            try {
+                tabPaneParent.setRotate(Float.parseFloat(angleTF.getText()));
+                angleErr.setText("");
+            } catch (Exception e) {
+                angleErr.setText("Incorrect Angle");
+            }
+            tabPane.requestFocus();
+        });
+        HBox rotation = new HBox();
+        rotation.setSpacing(5);
+        rotation.getChildren().addAll(rotLabel, rotate, rotateParent, angleContainer, angleErr);
+        addRemRotRev.getChildren().add(rotation);
+
+        Label reverseLabel = new Label("Reverse order of tabs");
+        reverseLabel.setMinWidth(LABEL_WIDTH);
+        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();
+        });
+        HBox revContainer = new HBox();
+        revContainer.setSpacing(5);
+        revContainer.getChildren().addAll(reverseLabel, reverse);
+        addRemRotRev.getChildren().add(revContainer);
+
+        actions.setOnMousePressed(event -> {
+            angleErr.setText("");
+            indexErr.setText("");
+        });
+        mainContent.getChildren().add(actions);
+        return mainContent;
+    }
+
     private void setupInternalTab() {
         StackPane internalTabContent = new StackPane();
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/TabObservableList.java	Sat Dec 09 00:42:02 2017 +0530
@@ -0,0 +1,84 @@
+/*
+ * 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;
+
+/*
+ * TabObservableList class extends ObservableListWrapper and
+ * adds a method for reordering the list.
+ */
+
+public class TabObservableList<E> extends ObservableListWrapper<E> {
+    private final List<E> tabList;
+
+    public TabObservableList(List<E> 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<E>(from, to + 1, perm, this));
+    }
+}
--- a/modules/javafx.controls/src/main/java/javafx/scene/control/TabPane.java	Thu Dec 07 18:58:31 2017 -0800
+++ b/modules/javafx.controls/src/main/java/javafx/scene/control/TabPane.java	Sat Dec 09 00:42:02 2017 +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<Tab> tabs = FXCollections.observableArrayList();
+    private ObservableList<Tab> tabs = new TabObservableList<>(new ArrayList<>());
 
     /**
      * <p>The tabs to display in this TabPane. Changing this ObservableList will
@@ -848,4 +848,54 @@
          */
         UNAVAILABLE
     }
+
+
+    // TabDragPolicy //
+    private ObjectProperty<TabDragPolicy> 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<TabDragPolicy> tabDragPolicyProperty() {
+        if (tabDragPolicy == null) {
+            tabDragPolicy = new SimpleObjectProperty<TabDragPolicy>(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.
+         * <p>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.</p>
+         */
+        REORDER
+    }
 }
--- a/modules/javafx.controls/src/main/java/javafx/scene/control/skin/TabPaneSkin.java	Thu Dec 07 18:58:31 2017 -0800
+++ b/modules/javafx.controls/src/main/java/javafx/scene/control/skin/TabPaneSkin.java	Sat Dec 09 00:42:02 2017 +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<Tab> tabs = tabPane.getTabs();
+                    if (dragState != DragState.REORDER) {
+                        TabPane tabPane = getSkinnable();
+                        List<Tab> 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<Tab> permutatedTabs = new ArrayList<Tab>(size);
-                    getSkinnable().getSelectionModel().clearSelection();
+                        // 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<Tab> permutatedTabs = new ArrayList<Tab>(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));
+                        // 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,341 @@
             default: return super.queryAccessibleAttribute(attribute, parameters);
         }
     }
+
+    // --------------------------
+    // Tab Reordering
+    // --------------------------
+    private enum DragState {
+        NONE,
+        START,
+        REORDER
+    }
+    private EventHandler<MouseEvent> headerDraggedHandler = this::handleHeaderDragged;
+    private EventHandler<MouseEvent> headerMousePressedHandler = this::handleHeaderMousePressed;
+    private EventHandler<MouseEvent> headerMouseReleasedHandler = this::handleHeaderMouseReleased;
+
+    private int dragTabHeaderIndex;
+    private TabHeaderSkin dragTabHeader;
+    private TabHeaderSkin dropTabHeader;
+    private StackPane headersRegion;
+    private DragState dragState;
+    private final int MIN_TO_MAX = 1;
+    private final int MAX_TO_MIN = -1;
+    private int xLayoutDirection;
+    private double dragEventPrevLoc;
+    private int prevDragDirection = MIN_TO_MAX;
+    private final double DRAG_DIST_THRESHOLD = 0.75;
+
+    // Reordering Animation
+    private final double ANIM_DURATION = 120;
+    private TabHeaderSkin dropAnimHeader;
+    private Tab swapTab;
+    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<Node>() {
+        public void onChanged(Change<? extends Node> 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 headersRegion) {
+        dragState = DragState.NONE;
+        this.headersRegion = headersRegion;
+        updateListeners();
+        getSkinnable().tabDragPolicyProperty().addListener((observable, oldValue, newValue) -> {
+            if (oldValue != newValue) {
+                updateListeners();
+            }
+        });
+    }
+
+    private void handleHeaderMousePressed(MouseEvent event) {
+        ((StackPane)event.getSource()).setMouseTransparent(true);
+        startDrag(event);
+    }
+
+    private void handleHeaderMouseReleased(MouseEvent event) {
+        ((StackPane)event.getSource()).setMouseTransparent(false);
+        stopDrag();
+        event.consume();
+    }
+
+    private void handleHeaderDragged(MouseEvent event) {
+        perfromDrag(event);
+    }
+
+    private double getDragDelta(double curr, double prev) {
+        if (getSkinnable().getSide().equals(Side.TOP) ||
+                getSkinnable().getSide().equals(Side.RIGHT)) {
+            return curr - prev;
+        } else {
+            return prev - curr;
+        }
+    }
+
+    private int deriveTabHeaderLayoutXDirection() {
+        if (getSkinnable().getSide().equals(Side.TOP) ||
+                getSkinnable().getSide().equals(Side.RIGHT)) {
+            // TabHeaderSkin are laid out in left to right direction inside headersRegion
+            return MIN_TO_MAX;
+        }
+        // TabHeaderSkin are laid out in right to left direction inside headersRegion
+        return MAX_TO_MIN;
+    }
+
+    private void perfromDrag(MouseEvent event) {
+        int dragDirection;
+        double dragHeaderNewLayoutX;
+        Bounds dragHeaderBounds;
+        Bounds dropHeaderBounds;
+        double draggedDist;
+        double mouseCurrentLoc = getHeaderRegionLocalX(event);
+        double dragDelta = getDragDelta(mouseCurrentLoc, dragEventPrevLoc);
+
+        if (dragDelta > 0) {
+            // Dragging the tab header towards higher indexed tab headers inside headersRegion.
+            dragDirection = MIN_TO_MAX;
+        } else {
+            // Dragging the tab header towards lower indexed tab headers inside headersRegion.
+            dragDirection = MAX_TO_MIN;
+        }
+        // Stop dropHeaderAnim if direction of drag is changed
+        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 == MIN_TO_MAX) {
+                // Dragging the tab header towards higher indexed tab headers
+                // Last tab header can not be dragged outside headersRegion.
+
+                // 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);
+
+                    // dropTabHeader should not be already reordering.
+                    if (dropAnimHeader != dropTabHeader) {
+                        dropHeaderBounds = dropTabHeader.getBoundsInParent();
+
+                        if (xLayoutDirection == MIN_TO_MAX) {
+                            draggedDist = dragHeaderBounds.getMaxX() - dropHeaderBounds.getMinX();
+                        } else {
+                            draggedDist = dropHeaderBounds.getMaxX() - dragHeaderBounds.getMinX();
+                        }
+
+                        // A tab header is reordered when dragged tab header crosses DRAG_DIST_THRESHOLD% of next tab header's width.
+                        if (draggedDist > dropHeaderBounds.getWidth() * DRAG_DIST_THRESHOLD) {
+                            stopAnim(dropHeaderAnim);
+                            // Distance by which tab header should be animated.
+                            dropHeaderTransitionX = xLayoutDirection * -dragHeaderBounds.getWidth();
+                            if (xLayoutDirection == MIN_TO_MAX) {
+                                dragHeaderDestX = dropHeaderBounds.getMaxX() - dragHeaderBounds.getWidth();
+                            } else {
+                                dragHeaderDestX = dropHeaderBounds.getMinX();
+                            }
+                            startHeaderReorderingAnim();
+                        } else {
+                            break;
+                        }
+                    }
+                }
+            } else {
+                // dragDirection is MAX_TO_MIN
+                // Dragging the tab header towards lower indexed tab headers.
+                // First tab header can not be dragged outside headersRegion.
+
+                // When the mouse is moved too fast, sufficient number of events
+                // are not generated. Hence it is required to check all possible
+                // tab headers to be reordered.
+                for (int i = dragTabHeaderIndex - 1; i >= 0; i--) {
+                    dropTabHeader = (TabHeaderSkin) headersRegion.getChildren().get(i);
+
+                    // dropTabHeader should not be already reordering.
+                    if (dropAnimHeader != dropTabHeader) {
+                        dropHeaderBounds = dropTabHeader.getBoundsInParent();
+
+                        if (xLayoutDirection == MIN_TO_MAX) {
+                            draggedDist = dropHeaderBounds.getMaxX() - dragHeaderBounds.getMinX();
+                        } else {
+                            draggedDist = dragHeaderBounds.getMaxX() - dropHeaderBounds.getMinX();
+                        }
+
+                        // A tab header is reordered when dragged tab crosses DRAG_DIST_THRESHOLD% of next tab header's width.
+                        if (draggedDist > dropHeaderBounds.getWidth() * DRAG_DIST_THRESHOLD) {
+                            stopAnim(dropHeaderAnim);
+                            // Distance by which tab header should be animated.
+                            dropHeaderTransitionX = xLayoutDirection * dragHeaderBounds.getWidth();
+                            if (xLayoutDirection == MIN_TO_MAX) {
+                                dragHeaderDestX = dropHeaderBounds.getMinX();
+                            } else {
+                                dragHeaderDestX = dropHeaderBounds.getMaxX() - dragHeaderBounds.getWidth();
+                            }
+                            startHeaderReorderingAnim();
+                        } else {
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        dragEventPrevLoc = mouseCurrentLoc;
+        event.consume();
+    }
+
+    private void startDrag(MouseEvent event) {
+        // Stop the animations if any are running from previous reorder.
+        stopAnim(dropHeaderAnim);
+        stopAnim(dragHeaderAnim);
+
+        dragTabHeader = (TabHeaderSkin) event.getSource();
+        if (dragTabHeader != null) {
+            dragState = DragState.START;
+            swapTab = null;
+            xLayoutDirection = deriveTabHeaderLayoutXDirection();
+            dragEventPrevLoc = getHeaderRegionLocalX(event);
+            dragTabHeaderIndex = headersRegion.getChildren().indexOf(dragTabHeader);
+            dragTabHeader.setViewOrder(0);
+            dragHeaderStartX = dragHeaderDestX = dragTabHeader.getLayoutX();
+        }
+    }
+
+    private double getHeaderRegionLocalX(MouseEvent ev) {
+        // The event is converted to tab header's parent i.e. headersRegion's local space.
+        // This will provide a value of X co-ordinate with all transformations of TabPane
+        // and transformations of all nodes in the TabPane's parent hierarchy.
+        Point2D sceneToLocalHR = headersRegion.sceneToLocal(ev.getSceneX(), ev.getSceneY());
+        return sceneToLocalHR.getX();
+    }
+
+    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<Tab>) 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();
+        }
+    }
 }
--- a/modules/javafx.controls/src/test/java/test/javafx/scene/control/TabPaneTest.java	Thu Dec 07 18:58:31 2017 -0800
+++ b/modules/javafx.controls/src/test/java/test/javafx/scene/control/TabPaneTest.java	Sat Dec 09 00:42:02 2017 +0530
@@ -442,6 +442,22 @@
         assertSame(tabPane.getTabClosingPolicy(), TabPane.TabClosingPolicy.UNAVAILABLE);
     }
 
+    @Test public void setTabDragPolicyAndSeeValueIsReflectedInModel() {
+        tabPane.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
+        assertSame(TabPane.TabDragPolicy.REORDER, tabPane.tabDragPolicyProperty().getValue());
+
+        tabPane.setTabDragPolicy(TabPane.TabDragPolicy.FIXED);
+        assertSame(TabPane.TabDragPolicy.FIXED, tabPane.tabDragPolicyProperty().getValue());
+    }
+
+    @Test public void setTabDragPolicyAndSeeValue() {
+        tabPane.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
+        assertSame(TabPane.TabDragPolicy.REORDER, tabPane.getTabDragPolicy());
+
+        tabPane.setTabDragPolicy(TabPane.TabDragPolicy.FIXED);
+        assertSame(TabPane.TabDragPolicy.FIXED, tabPane.getTabDragPolicy());
+    }
+
     @Test public void setRotateGraphicAndSeeValueIsReflectedInModel() {
         tabPane.setRotateGraphic(true);
         assertTrue(tabPane.rotateGraphicProperty().getValue());
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/system/src/test/java/test/robot/javafx/scene/TabPaneDragPolicyTest.java	Sat Dec 09 00:42:02 2017 +0530
@@ -0,0 +1,404 @@
+/*
+ * 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.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.fail;
+
+/*
+ * Unit test for verifying DragPolicies.
+ *
+ * There are 8 tests in this file.
+ * Steps of 4 tests for DragPolicy.REORDER
+ * 1. Create TabPane with 4 tabs.
+ * 2. Drag tab0 to last after tab3.
+ * 3. Verify that tab1 is the first tab after reorder.
+ * 4. Verify that a correct permutation change event is received.
+ * 5. Verify that getTabs() is also reordered correctly.
+ * Repeat the test for four Sides.
+ *
+ * Steps of 4 tests for DragPolicy.FIXED
+ * 1. Create TabPane with 4 tabs.
+ * 2. Drag tab0 to last after tab3, the tab0 should not get dragged.
+ * 3. Verify that tab0 is still the first tab.
+ * 4. Verify that permutation change event is not received.
+ * Repeat the test for four Sides.
+ */
+public class TabPaneDragPolicyTest {
+    CountDownLatch[] latches;
+    CountDownLatch changeListenerLatch;
+    static CountDownLatch startupLatch;
+    static Robot robot;
+    static TabPane tabPane;
+    static volatile Stage stage;
+    static volatile Scene scene;
+    static final int SCENE_WIDTH = 250;
+    static final int SCENE_HEIGHT = SCENE_WIDTH;
+    final int DRAG_DISTANCE = SCENE_WIDTH - 50;
+    final int DX = 15;
+    final int DY = DX;
+    Tab[] tabs;
+    Tab expectedTab;
+    Tab selectedTab;
+    final String PERMUTED_SEQ = "tab1tab2tab3tab0";
+    final String REORDER_SEQ = "tab1tab2tab3tab0";
+    boolean listenerTestResult = false;
+    ReorderChangeListener reorderListener = new ReorderChangeListener();
+    FixedChangeListener fixedListener = new FixedChangeListener();
+
+    class ReorderChangeListener implements ListChangeListener<Tab> {
+        @Override
+        public void onChanged(Change<? extends Tab> c) {
+            while (c.next()) {
+                if (c.wasPermutated()) {
+                    String list = "";
+                    for (int i = c.getFrom(); i < c.getTo(); i++) {
+                        list += tabPane.getTabs().get(i).getText();
+                    }
+                    listenerTestResult = list.equals(PERMUTED_SEQ);
+                    list = "";
+                    for (Tab t : tabPane.getTabs()) {
+                        list += t.getText();
+                    }
+                    listenerTestResult = listenerTestResult && list.equals(REORDER_SEQ);
+                    changeListenerLatch.countDown();
+                }
+            }
+        };
+    }
+
+    class FixedChangeListener implements ListChangeListener<Tab> {
+        @Override
+        public void onChanged(Change<? extends Tab> c) {
+            listenerTestResult = false;
+            changeListenerLatch.countDown();
+        };
+    }
+
+    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];
+        tabPane.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
+        tabPane.setSide(Side.TOP);
+        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", listenerTestResult);
+    }
+
+    @Test
+    public void testReorderBottom() {
+        expectedTab = tabs[1];
+        tabPane.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
+        tabPane.setSide(Side.BOTTOM);
+        tabPane.getTabs().addListener(reorderListener);
+        testReorder(DX, SCENE_HEIGHT - 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", listenerTestResult);
+    }
+
+    @Test
+    public void testReorderLeft() {
+        expectedTab = tabs[1];
+        tabPane.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
+        tabPane.setSide(Side.LEFT);
+        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", listenerTestResult);
+    }
+
+    @Test
+    public void testReorderRight() {
+        expectedTab = tabs[1];
+        tabPane.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
+        tabPane.setSide(Side.RIGHT);
+        tabPane.getTabs().addListener(reorderListener);
+        testReorder(SCENE_WIDTH - 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", listenerTestResult);
+    }
+
+    @Test
+    public void testFixedTop() {
+        expectedTab = tabs[0];
+        listenerTestResult = true;
+        tabPane.setTabDragPolicy(TabPane.TabDragPolicy.FIXED);
+        tabPane.setSide(Side.TOP);
+        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", listenerTestResult);
+    }
+
+    @Test
+    public void testFixedBottom() {
+        expectedTab = tabs[0];
+        listenerTestResult = true;
+        tabPane.setTabDragPolicy(TabPane.TabDragPolicy.FIXED);
+        tabPane.setSide(Side.BOTTOM);
+        tabPane.getTabs().addListener(fixedListener);
+        testReorder(DX, SCENE_HEIGHT - 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", listenerTestResult);
+    }
+
+    @Test
+    public void testFixedLeft() {
+        expectedTab = tabs[0];
+        listenerTestResult = true;
+        tabPane.setTabDragPolicy(TabPane.TabDragPolicy.FIXED);
+        tabPane.setSide(Side.LEFT);
+        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", listenerTestResult);
+    }
+
+    @Test
+    public void testFixedRight() {
+        expectedTab = tabs[0];
+        listenerTestResult = true;
+        tabPane.setTabDragPolicy(TabPane.TabDragPolicy.FIXED);
+        tabPane.setSide(Side.RIGHT);
+        tabPane.getTabs().addListener(fixedListener);
+        testReorder(SCENE_WIDTH - 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", listenerTestResult);
+    }
+
+    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) {
+            fail("Thread was interrupted." + ex);
+        }
+        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(changeListenerLatch, 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(changeListenerLatch, 5, "Timeout waiting ChangeListener to get called.");
+            waitForLatch(latches[1], 5, "Timeout waiting tabs[1] to get selected.");
+        }
+    }
+
+    public static class TestApp extends Application {
+        @Override
+        public void start(Stage primaryStage) {
+            robot = com.sun.glass.ui.Application.GetApplication().createRobot();
+            stage = primaryStage;
+            tabPane = new TabPane();
+            tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
+            scene = new Scene(tabPane, SCENE_WIDTH, SCENE_HEIGHT);
+            stage.setScene(scene);
+            stage.initStyle(StageStyle.UNDECORATED);
+            stage.addEventHandler(WindowEvent.WINDOW_SHOWN, e ->
+                    Platform.runLater(startupLatch::countDown));
+            stage.setAlwaysOnTop(true);
+            stage.show();
+        }
+    }
+
+    @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");
+    }
+
+    @AfterClass
+    public static void exit() {
+        Platform.runLater(() -> {
+            stage.hide();
+        });
+        Platform.exit();
+    }
+
+    @Before
+    public void setupTest() {
+        changeListenerLatch = new CountDownLatch(1);
+        latches = new CountDownLatch[4];
+        CountDownLatch latch = new CountDownLatch(1);
+        Platform.runLater(() -> {
+            tabs = new Tab[4];
+            for (int i = 0 ; i < 4; ++i) {
+                tabs[i] = new Tab("tab" + i);
+            }
+            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 setupTest().");
+    }
+
+    @After
+    public void resetTest() {
+        expectedTab = null;
+        selectedTab = null;
+        listenerTestResult = false;
+        CountDownLatch latch = new CountDownLatch(1);
+        Platform.runLater(() -> {
+            for (int i = 0 ; i < 4; ++i) {
+                tabs[i].setOnSelectionChanged(null);
+            }
+            tabPane.getTabs().removeAll(tabs);
+            tabs = null;
+            latch.countDown();
+        });
+        waitForLatch(latch, 5, "Timeout waiting for resetTest().");
+    }
+
+    public static void waitForLatch(CountDownLatch latch, int seconds, String msg) {
+        try {
+            if (!latch.await(seconds, TimeUnit.SECONDS)) {
+                System.out.println(msg);
+            }
+        } catch (Exception ex) {
+            fail("Unexpected exception: " + ex);
+        }
+    }
+}