I got JavaFX main thread, there I create new Thread that extends Task and sort and replace bars. Everyhing is good, but I want to make some delays(like 100ms) while replacing to show step by step sorting, maybe with animation. The problem is when I use Thread.sleep() or TranslateTransition() it just sum all delays miliseconds together in one big delay that happens before changing bars. How can I make delay that will work properly in UI thread?
In main class:
Sorting sorting = new Sorting();
sortThread = new Thread(sorting, "sort");
sortThread.start();
sortThread.join();
And my class Sorting extends Task
public class Sorting extends Task<Void> {
//some stuff here
#Override
protected Void call() throws Exception {
taskThread = new Thread(counter, "time");
taskThread.setDaemon(true);
taskThread.start();
int n = array_tmp.length;
int temp;
for (int i = 0; i < n; i++) {
for (int j = 1; j < (n - i); j++) {
if (array_tmp[j - 1] > array_tmp[j]) {
//replacing bars
Node n1 = barChart.getData().get(j-1).getData().get(0).getNode();
Node n2 = barChart.getData().get(j).getData().get(0).getNode();
double x1 = n1.getTranslateX() + ((barChart.getWidth()-69)/array_tmp.length);
double x2 = n2.getTranslateX() - ((barChart.getWidth()-69)/array_tmp.length);
n1.setTranslateX(x1);
n2.setTranslateX(x2);
barChart.getData().get(j-1).getData().get(0).setNode(n2);
barChart.getData().get(j).getData().get(0).setNode(n1);
temp = array_tmp[j - 1];
array_tmp[j - 1] = array_tmp[j];
array_tmp[j] = temp;
}
}
}
}
}
There are two basic rules to threading in JavaFX:
UI components (nodes) that are part of a scene graph that is actually displayed can only be accessed from the JavaFX application thread. Some other operations (such as creating a new Stage) are also subject to this rule.
Any long-running or blocking operation should be run on a background thread (i.e. not the JavaFX application thread). This is because the JavaFX application thread is the one needed to render the UI and respond to user interaction. Consequently, if you block the FX Application Thread, then the UI cannot be rendered and the application will become unresponsive until your operation completes.
The javafx.concurrent API provides facilities for managing code that can be run on background threads and executing callbacks on the FX application thread.
The javafx.animation API additionally provides classes that allow UI code to be executed on the JavaFX application thread at specific times. Note that the animation API avoids creating background threads at all.
So for your use case, if you want to animate the swapping of two bars in the bar chart, you can do so with the animation API. A general method that creates an animation that performs such a swap might look like this:
private <T> Animation createSwapAnimation(Data<?, T> first, Data<?, T> second) {
double firstX = first.getNode().getParent().localToScene(first.getNode().getBoundsInParent()).getMinX();
double secondX = first.getNode().getParent().localToScene(second.getNode().getBoundsInParent()).getMinX();
double firstStartTranslate = first.getNode().getTranslateX();
double secondStartTranslate = second.getNode().getTranslateX();
TranslateTransition firstTranslate = new TranslateTransition(Duration.millis(500), first.getNode());
firstTranslate.setByX(secondX - firstX);
TranslateTransition secondTranslate = new TranslateTransition(Duration.millis(500), second.getNode());
secondTranslate.setByX(firstX - secondX);
ParallelTransition translate = new ParallelTransition(firstTranslate, secondTranslate);
translate.statusProperty().addListener((obs, oldStatus, newStatus) -> {
if (oldStatus == Animation.Status.RUNNING) {
T temp = first.getYValue();
first.setYValue(second.getYValue());
second.setYValue(temp);
first.getNode().setTranslateX(firstStartTranslate);
second.getNode().setTranslateX(secondStartTranslate);
}
});
return translate;
}
The basic idea here is pretty simple: we measure the distance in the x-coordinates between the two nodes; make a note of their current translateX properties, and then create two transitions which move the nodes so they take each others positions. Those two transitions are executed in parallel. When the transitions are complete (indicated by the status of the transition changing from RUNNING to something else), the values in the chart are exchanged and the translateX properties reset to their previous values (the effect of these will cancel out visually, but now the chart data will reflect the fact that the two have been exchanged).
If you want to perform a sort algorithm which animates the exchanges in the sorting, pausing between each step of the algorithm, you can do this using a background thread (you may be able to do this with an animation too - but this seems simple enough and is perhaps more instructional).
The idea here is to create a Task whose call() method performs the sort algorithm, pausing at various points to allow the use to see what is happening. Because we are pausing (blocking), this cannot be run on the FX Application Thread, as the blocking would prevent the UI being updated until the entire process was complete.
Here is an implementation of a bubble sort (for simplicity). On each iteration of the sort, we:
highlight the two bars to be compared in green*
pause so the user can see that
if the values need to be exchanged:
get the animation defined above and run it*
pause again, and
reset the colors*.
Steps marked * in the above psuedocode change the UI, so they must be executed on the FX Application thread, so they need to be wrapped in a call to Platform.runLater(...), which causes the provided code to be executed on the FX Application Thread.
The last tricky part here (and this is unusually tricky) is that the animation, of course, takes some time to execute. So we must arrange for our background thread to wait until the animation is complete. We do this by creating a CountDownLatch with a count of 1. When the animation is complete, we count the latch down. Then after submitting the animation to Platform.runLater(..), our background thread just waits for the latch to count down before proceeding, by calling latch.await(). It is quite unusual for a background thread to need to wait for something to run on the FX Application Thread, but this is one technique to do that in a case where you do need it.
The implementation of the bubble sort thus looks like
private Task<Void> createSortingTask(Series<String, Number> series) {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
ObservableList<Data<String, Number>> data = series.getData();
for (int i = data.size() - 1; i >= 0; i--) {
for (int j = 0 ; j < i; j++) {
Data<String, Number> first = data.get(j);
Data<String, Number> second = data.get(j + 1);
Platform.runLater(() -> {
first.getNode().setStyle("-fx-background-color: green ;");
second.getNode().setStyle("-fx-background-color: green ;");
});
Thread.sleep(500);
if (first.getYValue().doubleValue() > second.getYValue().doubleValue()) {
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
Animation swap = createSwapAnimation(first, second);
swap.setOnFinished(e -> latch.countDown());
swap.play();
});
latch.await();
}
Thread.sleep(500);
Platform.runLater(() -> {
first.getNode().setStyle("");
second.getNode().setStyle("");
});
}
}
return null;
}
};
}
Here is a complete demo. Since the sort algorithm, with its pauses, is encapsulated as a Task, we can leverage its callbacks and state properties if we need. As an example, we disable the buttons before starting the task, and use the onSucceeded handler to enable them again when it completes. It would be easy to add a "cancel" option too.
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.animation.Animation;
import javafx.animation.ParallelTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class AnimatedBubbleSort extends Application {
private Random rng = new Random();
private ExecutorService exec = Executors.newCachedThreadPool(runnable -> {
Thread t = new Thread(runnable);
t.setDaemon(true);
return t;
});
#Override
public void start(Stage primaryStage) {
BarChart<String, Number> chart = new BarChart<>(new CategoryAxis(), new NumberAxis());
chart.setAnimated(false);
Series<String, Number> series = generateRandomIntegerSeries(10);
chart.getData().add(series);
Button sort = new Button("Sort");
Button reset = new Button("Reset");
reset.setOnAction(e -> chart.getData().set(0, generateRandomIntegerSeries(10)));
HBox buttons = new HBox(5, sort, reset);
buttons.setAlignment(Pos.CENTER);
buttons.setPadding(new Insets(5));
sort.setOnAction(e -> {
Task<Void> animateSortTask = createSortingTask(chart.getData().get(0));
buttons.setDisable(true);
animateSortTask.setOnSucceeded(event -> buttons.setDisable(false));
exec.submit(animateSortTask);
});
BorderPane root = new BorderPane(chart);
root.setBottom(buttons);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private Task<Void> createSortingTask(Series<String, Number> series) {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
ObservableList<Data<String, Number>> data = series.getData();
for (int i = data.size() - 1; i >= 0; i--) {
for (int j = 0 ; j < i; j++) {
Data<String, Number> first = data.get(j);
Data<String, Number> second = data.get(j + 1);
Platform.runLater(() -> {
first.getNode().setStyle("-fx-background-color: green ;");
second.getNode().setStyle("-fx-background-color: green ;");
});
Thread.sleep(500);
if (first.getYValue().doubleValue() > second.getYValue().doubleValue()) {
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
Animation swap = createSwapAnimation(first, second);
swap.setOnFinished(e -> latch.countDown());
swap.play();
});
latch.await();
}
Thread.sleep(500);
Platform.runLater(() -> {
first.getNode().setStyle("");
second.getNode().setStyle("");
});
}
}
return null;
}
};
}
private <T> Animation createSwapAnimation(Data<?, T> first, Data<?, T> second) {
double firstX = first.getNode().getParent().localToScene(first.getNode().getBoundsInParent()).getMinX();
double secondX = first.getNode().getParent().localToScene(second.getNode().getBoundsInParent()).getMinX();
double firstStartTranslate = first.getNode().getTranslateX();
double secondStartTranslate = second.getNode().getTranslateX();
TranslateTransition firstTranslate = new TranslateTransition(Duration.millis(500), first.getNode());
firstTranslate.setByX(secondX - firstX);
TranslateTransition secondTranslate = new TranslateTransition(Duration.millis(500), second.getNode());
secondTranslate.setByX(firstX - secondX);
ParallelTransition translate = new ParallelTransition(firstTranslate, secondTranslate);
translate.statusProperty().addListener((obs, oldStatus, newStatus) -> {
if (oldStatus == Animation.Status.RUNNING) {
T temp = first.getYValue();
first.setYValue(second.getYValue());
second.setYValue(temp);
first.getNode().setTranslateX(firstStartTranslate);
second.getNode().setTranslateX(secondStartTranslate);
}
});
return translate;
}
private Series<String, Number> generateRandomIntegerSeries(int n) {
Series<String, Number> series = new Series<>();
for (int i = 1; i <= n; i++) {
series.getData().add(new Data<>(Integer.toString(i), rng.nextInt(90) + 10));
}
return series;
}
public static void main(String[] args) {
launch(args);
}
}
Related
I have coded myself into a corner here. In my FXML file, I declare a progress bar and an upload progress label (for a batch upload):
Program_view.fxml
<Label fx:id="uploadProgressLabel" layoutX="384" layoutY="579" text="Upload Progress">
<font>
<Font size="18" />
</font>
</Label>
...
<ProgressBar fx:id="uploadProgressBar" layoutX="185" layoutY="606" prefHeight="18" prefWidth="534" progress="0" />
Then I have a UI controller where I import all my elements from FXML:
UI_Controller.java
#FXML Label uploadProgressLabel;
#FXML ProgressBar uploadProgressBar;
Later in the UI Controller, there is a button whose action is to update the "upload" progress bar, and it doesn't. I've tried a few different thread / task strategies and none of them seem to work while they are running.
UI_Controller.java
#FXML
protected void distributeSetButtonClick() {
//In a previous version of this project using swing, I tossed this whole function into a new thread and that made the progress bar happy
//new Thread(() -> {
final boolean[] done = {false};
if (logTextArea.getText().equalsIgnoreCase("This is your console log, logs relating to uploading your set will appear here")) {
logTextArea.setText("");
}
//Upload each file and iterate label + counter
for (int i = 1; i <= uploadImages.size(); i++) {
System.out.println("Test: " + i);
uploadProgressLabel.setText("Uploading image " + i + "/" + uploadImages.size());
File f = uploadImages.get(i - 1);
mediaIds.add(UploadFile.uploadFile(f, logTextArea, i - 1, uploadImages.size()));
double currentProgress = (1.0 / uploadImages.size()) * i;
uploadProgressBar.setProgress(currentProgress);
}
uploadProgressLabel.setText("Completed uploading: " + uploadImages.size() + " images");
String areaUpdate = filesSelectedTextArea.getText();
if (mediaIds.size() == uploadImages.size()) {
areaUpdate += "\r\n\r\n All Images uploaded successfully";
} else {
areaUpdate += "\r\n\r\n One or more files had an error while uploading";
}
filesSelectedTextArea.setText(areaUpdate);
}
...
My question is, how can I get the progress bar / label to update while they are on the main thread? When I try moving them off the main thread I get an error about them not being on the JavaFX thread. I've also tried moving the logic over into a task, which looked like this (and then had a run of the task on the main thread) also to no avail:
Tasker.java
public static Task<Void> updateProgressBar(ProgressBar p, double value) {
Task<Void> task = new Task<Void>() {
#Override
protected Void call() throws Exception {
p.setProgress(value);
return null;
}
};
p.progressProperty().bind(task.progressProperty());
return task;
}
Some guidance would be appreciated.
Like most other UI toolkits, JavaFX is single threaded, with the FX Application Thread being responsible for processing user events and rendering the UI. This means:
You must not update UI controls from a background thread. JavaFX will throw IllegalStateExceptions in many (though not all) cases if you do this. (Note that Swing doesn't throw exceptions; it just leaves you vulnerable to arbitrary failure at some indeterminate point.)
Long-running processes (such as your file upload) must not be run on the FX Application Thread. Since this thread is responsible for rendering the UI and processing user events, no UI updates (such as updating the label and progress bar) will be possible until the long-running process is complete. Additionally, the UI will be unresponsive during this time.
You should use a Task to implement the long running process, and run the task on a background thread. The Task has thread-safe update methods which will update its properties (such as progress and message) on the FX Application Thread, so you can safely bind properties of UI elements to these properties. It also has onSucceeded and onFailed handlers, which are also executed on the FX Application thread. The onSucceeded handler can access any return value from the task.
So your code should look something like:
#FXML
protected void distributeSetButtonClick(){
//In a previous version of this project using swing, I tossed this whole function into a new thread and that made the progress bar happy
Task<String> task = new Task<>() {
#Override
protected String call() throws Exception {
final boolean[] done = {false};
//Upload each file and iterate label + counter
for (int i = 1; i <= uploadImages.size(); i++) {
System.out.println("Test: " + i);
updateMessage("Uploading image " + i + "/" + uploadImages.size());
File f = uploadImages.get(i - 1);
mediaIds.add(UploadFile.uploadFile(f, logTextArea, i - 1, uploadImages.size()));
updateProgress(i, uploadImages.size());
}
if (mediaIds.size() == uploadImages.size()) {
return "All Images uploaded successfully";
} else {
return "One or more files had an error while uploading";
}
}
};
if (logTextArea.getText().equalsIgnoreCase("This is your console log, logs relating to uploading your set will appear here")) {
logTextArea.setText("");
}
task.setOnSucceeded(event -> {
filesSelectedTextArea.append("\n\n"+task.getValue());
uploadProgressLabel.setText("Completed uploading: " + uploadImages.size() + " images");
});
uploadProgressLabel.textProperty().unbind()
uploadProgressLabel.textProperty().bind(task.messageProperty());
uploadProgressBar.progressProperty().unbind();
uploadProgressBar.progressProperty().bind(task.progressProperty());
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
I am moving a point every so often, the problem is that to keep the point inside the map and not get lost as it moves, I have to reload the map. How could you avoid recharging it, since the movement occurs every two seconds and the map is reloaded every two seconds is too uncomfortable.
Here the code:
cont++;
final long EXECUTION_TIME = 2000;
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
int aux = 0;
#Override
public void run() {
GraphicsOverlay graphicsOverlay1 = new GraphicsOverlay();
Graphic g1 = new Graphic(getLatLong(aux), attributes, sms);
graphicsOverlay1.getGraphics().add(g1);
mMap.getGraphicsOverlays().add(graphicsOverlay1);
map = new ArcGISMap(basemapType, getLatLong(aux).getY(), getLatLong(aux).getX(), 17);
mMap.setMap(map); //Here is where the map is reloaded, some other way to avoid this burden
handler.postDelayed(this, EXECUTION_TIME);
}
)};
You must use the method: SetViewpointCenterAsync in your mMap and thus avoid loading the map when updating points on the map.
The code would look like this:
map = new ArcGISMap(basemapType, getLatLong(aux).getY(), getLatLong(aux).getX(), 17);
mMap.setMap(map);
cont++;
final long EXECUTION_TIME = 2000;
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
int aux = 0;
#Override
public void run() {
GraphicsOverlay graphicsOverlay1 = new GraphicsOverlay();
Graphic g1 = new Graphic(getLatLong(aux), attributes, sms);
graphicsOverlay1.getGraphics().add(g1);
mMap.getGraphicsOverlays().add(graphicsOverlay1);
mMap.setViewpointCenterAsync(new Point( getLatLong(aux).getX(), getLatLong(aux).getY(),SpatialReferences.getWgs84()),6000.0) ;
handler.postDelayed(this, EXECUTION_TIME); } )};
I got CyclicBarrier code from oracle page to understand it more. I modified it and now having one doubt.
Below code doesn't terminate but If I uncomment Thread.sleep condition, It works fine.
import java.util.Arrays;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
class Solver {
final int N;
final float[][] data;
boolean done = false;
final CyclicBarrier barrier;
class Worker implements Runnable {
int myRow;
Worker(int row) {
myRow = row;
}
public void run() {
while (!done) {
processRow(myRow);
try {
barrier.await();
} catch (InterruptedException ex) {
return;
} catch (BrokenBarrierException ex) {
return;
}
}
System.out.println("Run finish for " + Thread.currentThread().getName());
}
private void processRow(int row) {
float[] rowData = data[row];
for (int i = 0; i < rowData.length; i++) {
rowData[i] = 1;
}
/*try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
done = true;
}
}
public Solver(float[][] matrix) {
data = matrix;
N = matrix.length;
barrier = new CyclicBarrier(N, new Runnable() {
public void run() {
for (int i = 0; i < data.length; i++) {
System.out.println("Data " + Arrays.toString(data[i]));
}
System.out.println("Completed:");
}
});
for (int i = 0; i < N; ++i)
new Thread(new Worker(i), "Thread "+ i).start();
}
}
public class CyclicBarrierTest {
public static void main(String[] args) {
float[][] matrix = new float[5][5];
Solver solver = new Solver(matrix);
}
}
Why Thread.sleep is required in above code?
I've not run your code but there may be a race condition, here is a scenario that reveals it:
you start the first thread, it runs during a certain amount of time sufficient for it to finish the processRow method call so it sets done to true and then waits on the barrier,
the other threads start but they see that all is "done" so they don't enter the loop and they'll never wait on the barrier, and end directly
the barrier will never be activated as only one of the N threads has reached it
deadlock
Why it is working with the sleep:
when one of the thread starts to sleep it lets the other threads work before marking the work as "done"
the other threads have enough time to work and can themselves reach the barrier
2 seconds is largely enough for 5 threads to end a processing that should not last longer than 10ms
But note that if your system is ovrerloaded it could too deadlock:
the first thread starts to sleep
the OS scheduler lets another application work during more than 2 seconds
the OS scheduler comes back to your application and the threads scheduler chooses the first thread again and lets it terminate, setting done to true
and here again the first scenario => deadlock too
And a possible solution (sorry not tested):
change your while loops for do/while loops:
do
{
processRow(myRow);
...
}
while (!done);
I have a Tabpane with multiple tabs.
I want to re-position tabs by just dragging them at a particular position(just like the way we are able to arrange tabs in browser.)
Is there any way i can achieve it?
We achieved it in a slightly different way.Instead of drag/drop feature we provided the move left/move right functionality on tab context menu which in turns moves the tab.
We wanted to have this feature on priority so implemented it with this workaround for now.
Code snippet for MoveRight:
public void moveRight() {
protected TabPane workBook;
int cTabIndex = bem.workBook.getTabs().indexOf(bem.activeSheet);
int tabCount = workBook.getTabs().size();
if (tabCount > 1 && cTabIndex > 0) {
workBook.getTabs().remove(bem.activeSheet);
workBook.getTabs().add(cTabIndex - 1, bem.activeSheet);
}
}
I've implemented a class that handles both draggable and detachable tabs - more details here. The implementation is not the tidiest, nor the most resilient but works pretty well for me in the simple cases I've tried so far. I've deliberately kept everything in the one class to make it easier for others to copy / use / modify as they see fit.
The basic concept that I'm using (arguably mis-using) is that the graphic you can set on a tab can be any node, not just an ImageView (or similar.) So instead of using the setText() on Tab directly, I'm not adding any text at all, just setting the graphic to be a Label containing the desired text. Now that the label is present in the tab header (and is pretty much the tab header spacially), that makes it much easier (and skin-independant) to grab the global co-ordinates of each tab header in the pane. From then it's just a case of some relatively simple positioning logic to work out when to detach tabs into a new window, when to re-add them and when to reorder them.
Of course, this isn't an ideal solution but unfortunately I haven't seen much else on the subject!
import java.util.HashSet;
import java.util.Set;
import javafx.collections.ListChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.WindowEvent;
/**
* A draggable tab that can optionally be detached from its tab pane and shown
* in a separate window. This can be added to any normal TabPane, however a
* TabPane with draggable tabs must *only* have DraggableTabs, normal tabs and
* DrragableTabs mixed will cause issues!
* <p>
* #author Michael Berry
*/
public class DraggableTab extends Tab {
private static final Set<TabPane> tabPanes = new HashSet<>();
private Label nameLabel;
private Text dragText;
private static final Stage markerStage;
private Stage dragStage;
private boolean detachable;
static {
markerStage = new Stage();
markerStage.initStyle(StageStyle.UNDECORATED);
Rectangle dummy = new Rectangle(3, 10, Color.web("#555555"));
StackPane markerStack = new StackPane();
markerStack.getChildren().add(dummy);
markerStage.setScene(new Scene(markerStack));
}
/**
* Create a new draggable tab. This can be added to any normal TabPane,
* however a TabPane with draggable tabs must *only* have DraggableTabs,
* normal tabs and DrragableTabs mixed will cause issues!
* <p>
* #param text the text to appear on the tag label.
*/
public DraggableTab(String text) {
nameLabel = new Label(text);
setGraphic(nameLabel);
detachable = true;
dragStage = new Stage();
dragStage.initStyle(StageStyle.UNDECORATED);
StackPane dragStagePane = new StackPane();
dragStagePane.setStyle("-fx-background-color:#DDDDDD;");
dragText = new Text(text);
StackPane.setAlignment(dragText, Pos.CENTER);
dragStagePane.getChildren().add(dragText);
dragStage.setScene(new Scene(dragStagePane));
nameLabel.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
dragStage.setWidth(nameLabel.getWidth() + 10);
dragStage.setHeight(nameLabel.getHeight() + 10);
dragStage.setX(t.getScreenX());
dragStage.setY(t.getScreenY());
dragStage.show();
Point2D screenPoint = new Point2D(t.getScreenX(), t.getScreenY());
tabPanes.add(getTabPane());
InsertData data = getInsertData(screenPoint);
if(data == null || data.getInsertPane().getTabs().isEmpty()) {
markerStage.hide();
}
else {
int index = data.getIndex();
boolean end = false;
if(index == data.getInsertPane().getTabs().size()) {
end = true;
index--;
}
Rectangle2D rect = getAbsoluteRect(data.getInsertPane().getTabs().get(index));
if(end) {
markerStage.setX(rect.getMaxX() + 13);
}
else {
markerStage.setX(rect.getMinX());
}
markerStage.setY(rect.getMaxY() + 10);
markerStage.show();
}
}
});
nameLabel.setOnMouseReleased(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
markerStage.hide();
dragStage.hide();
if(!t.isStillSincePress()) {
Point2D screenPoint = new Point2D(t.getScreenX(), t.getScreenY());
TabPane oldTabPane = getTabPane();
int oldIndex = oldTabPane.getTabs().indexOf(DraggableTab.this);
tabPanes.add(oldTabPane);
InsertData insertData = getInsertData(screenPoint);
if(insertData != null) {
int addIndex = insertData.getIndex();
if(oldTabPane == insertData.getInsertPane() && oldTabPane.getTabs().size() == 1) {
return;
}
oldTabPane.getTabs().remove(DraggableTab.this);
if(oldIndex < addIndex && oldTabPane == insertData.getInsertPane()) {
addIndex--;
}
if(addIndex > insertData.getInsertPane().getTabs().size()) {
addIndex = insertData.getInsertPane().getTabs().size();
}
insertData.getInsertPane().getTabs().add(addIndex, DraggableTab.this);
insertData.getInsertPane().selectionModelProperty().get().select(addIndex);
return;
}
if(!detachable) {
return;
}
final Stage newStage = new Stage();
final TabPane pane = new TabPane();
tabPanes.add(pane);
newStage.setOnHiding(new EventHandler<WindowEvent>() {
#Override
public void handle(WindowEvent t) {
tabPanes.remove(pane);
}
});
getTabPane().getTabs().remove(DraggableTab.this);
pane.getTabs().add(DraggableTab.this);
pane.getTabs().addListener(new ListChangeListener<Tab>() {
#Override
public void onChanged(ListChangeListener.Change<? extends Tab> change) {
if(pane.getTabs().isEmpty()) {
newStage.hide();
}
}
});
newStage.setScene(new Scene(pane));
newStage.initStyle(StageStyle.UTILITY);
newStage.setX(t.getScreenX());
newStage.setY(t.getScreenY());
newStage.show();
pane.requestLayout();
pane.requestFocus();
}
}
});
}
/**
* Set whether it's possible to detach the tab from its pane and move it to
* another pane or another window. Defaults to true.
* <p>
* #param detachable true if the tab should be detachable, false otherwise.
*/
public void setDetachable(boolean detachable) {
this.detachable = detachable;
}
/**
* Set the label text on this draggable tab. This must be used instead of
* setText() to set the label, otherwise weird side effects will result!
* <p>
* #param text the label text for this tab.
*/
public void setLabelText(String text) {
nameLabel.setText(text);
dragText.setText(text);
}
private InsertData getInsertData(Point2D screenPoint) {
for(TabPane tabPane : tabPanes) {
Rectangle2D tabAbsolute = getAbsoluteRect(tabPane);
if(tabAbsolute.contains(screenPoint)) {
int tabInsertIndex = 0;
if(!tabPane.getTabs().isEmpty()) {
Rectangle2D firstTabRect = getAbsoluteRect(tabPane.getTabs().get(0));
if(firstTabRect.getMaxY()+60 < screenPoint.getY() || firstTabRect.getMinY() > screenPoint.getY()) {
return null;
}
Rectangle2D lastTabRect = getAbsoluteRect(tabPane.getTabs().get(tabPane.getTabs().size() - 1));
if(screenPoint.getX() < (firstTabRect.getMinX() + firstTabRect.getWidth() / 2)) {
tabInsertIndex = 0;
}
else if(screenPoint.getX() > (lastTabRect.getMaxX() - lastTabRect.getWidth() / 2)) {
tabInsertIndex = tabPane.getTabs().size();
}
else {
for(int i = 0; i < tabPane.getTabs().size() - 1; i++) {
Tab leftTab = tabPane.getTabs().get(i);
Tab rightTab = tabPane.getTabs().get(i + 1);
if(leftTab instanceof DraggableTab && rightTab instanceof DraggableTab) {
Rectangle2D leftTabRect = getAbsoluteRect(leftTab);
Rectangle2D rightTabRect = getAbsoluteRect(rightTab);
if(betweenX(leftTabRect, rightTabRect, screenPoint.getX())) {
tabInsertIndex = i + 1;
break;
}
}
}
}
}
return new InsertData(tabInsertIndex, tabPane);
}
}
return null;
}
private Rectangle2D getAbsoluteRect(Control node) {
return new Rectangle2D(node.localToScene(node.getLayoutBounds().getMinX(), node.getLayoutBounds().getMinY()).getX() + node.getScene().getWindow().getX(),
node.localToScene(node.getLayoutBounds().getMinX(), node.getLayoutBounds().getMinY()).getY() + node.getScene().getWindow().getY(),
node.getWidth(),
node.getHeight());
}
private Rectangle2D getAbsoluteRect(Tab tab) {
Control node = ((DraggableTab) tab).getLabel();
return getAbsoluteRect(node);
}
private Label getLabel() {
return nameLabel;
}
private boolean betweenX(Rectangle2D r1, Rectangle2D r2, double xPoint) {
double lowerBound = r1.getMinX() + r1.getWidth() / 2;
double upperBound = r2.getMaxX() - r2.getWidth() / 2;
return xPoint >= lowerBound && xPoint <= upperBound;
}
private static class InsertData {
private final int index;
private final TabPane insertPane;
public InsertData(int index, TabPane insertPane) {
this.index = index;
this.insertPane = insertPane;
}
public int getIndex() {
return index;
}
public TabPane getInsertPane() {
return insertPane;
}
}
}
I just found out that this has been implemented in JavaFX 10.
tabPane.tabDragPolicy = TabPane.TabDragPolicy.REORDER
...does the trick.
Update Feb 2016
There is an open feature request you can use to track implementation:
JDK-8092098 [TabPane] Support for draggable tabs
The feature request is currently scheduled for implementation in Java 9. Patches for obtaining drag and drop functionality are attached to the feature request.
Drag and Drop for tab headers is not implemented in the base JavaFX 2.2 platform.
Until that is implemented in the standard JDK, you will need to implement the feature yourself using JavaFX's Drag and Drop functionality. A similar feature is implemented for dragging table column headers, so perhaps you could look to the TableColumnHeader.java code for inspiration in implementing your feature.
Should you implement it (if you wish) you can contribute the modifications back to OpenJFX via patches to the TabSkin.java source.
A very descriptive answer can be found where you can create custom tabs for the same:
http://0divides0.wordpress.com/2010/10/21/movable-tabbed-panes-in-javafx/
A JavaFX cooked solution is hard to find as dev blog for the same states that such functionality is not present for Tabs and they plan to incorporate later.
http://grokbase.com/p/openjdk/openjfx-dev/123fq9k310/draggable-tabs
The following code shows how to solve the problem in a very simple way without tricks.
.....
.....
Tab tab1 = new Tab("Tab1");
Tab tab2 = new Tab("Tab21");
TabPane tabPane = new TabPane(tab1, tab21);
root.getChildren().add(tabPane);
....
....
System.out.println("Tabs size()= " + tabPane.lookupAll(".tab").size());
tabPane.lookupAll(".tab").forEach(t -> {
System.err.println("tab.bounds = " + t.getLayoutBounds());
});
You can get an access to other areas of TabPane by using style classes such as tab-content-area, tab-header-area, tab-header-background, headers-region, control-buttons-tab. Just use lookup or lookupAll methods of TabPane
So, I made a class that takes arrays and calculates a value from them. I then decided (unknowingly) to incorporate it into a GUI interface. All went well until I noticed this strange error; one of the jtextfields (prarie) would not store text while the other (yard) does.
I looked around and found my problem similiar to mine on this site;
Updating text in a JTextField
But he had one that doesn't work at all, where I have one that works and one that doesn't.
The Code is here (it's a bit long, but most of it is GUI), so hold your breath!:
import java.awt.GridLayout;
import javax.swing.Box;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
public class Window {
/**
* #param args
*/
private static int numb;
private static double micro, centi;
private static JTextField[] yard,prarie;
private static double[] charges,distances;
public static void main(String[] args)
{
//create a small dialog window to take in number of charged objects
JPanel startup = new JPanel();
JTextField many = new JTextField(5);
startup.add(many);
int result = JOptionPane.showConfirmDialog(null,startup , "Please Enter How Many Charged Objects are Being Evaluated", JOptionPane.OK_CANCEL_OPTION);
many.requestFocusInWindow();
//once ok is clicked, then the number input will be stored under 'numb'
//then proceed to inputFields
if (result == JOptionPane.OK_OPTION)
{
numb = Integer.parseInt(many.getText());
inputFields();
}
}
//this window opens the various JTextFields for input
public static void inputFields()
{
//top JTextFields
yard = new JTextField[numb];
JPanel chargePanel = new JPanel();
for(int x=0;x<numb;x++)
{
yard[x] =new JTextField(5);
chargePanel.add(new JLabel("Charge "+ Integer.toString(x+1)+":"));
chargePanel.add(yard[x]);
chargePanel.add(Box.createVerticalStrut(15)); // a spacer
}
//bottom JTextFields
prarie = new JTextField[numb-1];
JPanel meterPanel = new JPanel();
for(int x=0;x<numb-1;x++)
{
prarie[x]=new JTextField(5);
meterPanel.add(new JLabel("Meters "+ Integer.toString(x+1)+":"));
meterPanel.add(new JTextField(5));
meterPanel.add(Box.createVerticalStrut(15)); // a spacer
}
//JCheckBoxes
JCheckBox isMicro = new JCheckBox("Charges are in terms of microCoulombs");
JCheckBox isCm = new JCheckBox("Distances are in terms of centiMeters");
JPanel chechBox = new JPanel();
chechBox.add(isMicro);
chechBox.add(Box.createVerticalStrut(20));
chechBox.add(isCm);
//Paste them all together into one window
GridLayout gufi = new GridLayout(3,1);
JPanel host = new JPanel(gufi);
host.add(chargePanel);
host.add(meterPanel);
host.add(chechBox);
int result1 = JOptionPane.showConfirmDialog(null, host, "Please Enter Charge and Distance Values", JOptionPane.OK_CANCEL_OPTION);
//if ok is clicked, then go to 'printArr()' to print the JTextFields
//then go to assign the values from the JTextFields to private double arrays 'yard' and 'prarie'
if (result1 == JOptionPane.OK_OPTION)
{
micro = (isMicro.isSelected())? Math.pow(10, -6): 1;
centi = (isCm.isSelected())? .01: 1;
printArr();
assign();
}
}
//a makeshift method to print the value from the JTextFields
//to fix the problem of why prarie wouldn't store numbers
public static void printArr()
{
System.out.println("Charges are:");
for(int x=0;x<numb;x++)
System.out.print(yard[x].getText() + " ");
System.out.println("Distances are:");
for(int x=0;x<numb-1;x++)
System.out.print(prarie[x].getText() + " ");
}
//assigns values from JTextFields to the private double arrays 'yard' and 'prarie'
public static void assign()
{
try {
charges = new double[numb];
for(int x=0;x<numb;x++)
charges[x]=micro*Double.parseDouble(yard[x].getText().trim());
distances = new double[numb-1];
for(int x=0;x<numb-1;x++)
distances[x]=centi*Double.parseDouble(prarie[x].getText().trim());
} catch (NumberFormatException e) {
e.printStackTrace();
//inputFields();
}
calculate();
}
public static void calculate()
{
JPanel sample = new JPanel();
JTextField whichOne = new JTextField(5);
sample.add(whichOne);
int result = JOptionPane.showConfirmDialog(null,sample , "Please Enter Which Charged Object thy Wishs For", JOptionPane.OK_CANCEL_OPTION);
whichOne.requestFocusInWindow();
if (result == JOptionPane.OK_OPTION)
{
int target = Integer.parseInt(whichOne.getText());
}
}
}
Anyone who runs the code and takes the time to enter dummy values will see that 'yard' stores values while 'prarie' does not. Why is this?
*I'm pretty sure I'm overlooking obvious (as always).
Change:
meterPanel.add(new JTextField(5));
to:
meterPanel.add(prarie[x]);
in the for loop for the prarie textfields