Correct sizing of Webview embedded in Tabelcell - javafx

As mentioned here and here there is no easy way to determine the required height of a webview, until "RT-25005 Automatic preferred sizing of WebView" is implemented.
Are there any workarounds to this issue? I couldn't find a solution in SO or elsewhere. However since i think this is no uncommon problem, there needs to be a workaround. Any idea?
For Webviewsembeded in a stage I found the following solution (see here):
webView.getEngine().executeScript(
"window.getComputedStyle(document.body, null).getPropertyValue('height')"
);
or
Double.parseDouble(webView.getEngine().executeScript("document.height").toString())
However this doesn't seem to work for Webviews embedded in a treecell, like here. In this case I always get too big numbers as a result.
Minimal running example (including recommendation of jewelsea):
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.util.Callback;
import org.w3c.dom.Document;
public class TableViewSampleHTML extends Application {
private final ObservableList<MyData> data = FXCollections.observableArrayList(new MyData(1L), new MyData(3L), new MyData(2L), new MyData(4L), new MyData(1L));
public static void main(final String[] args) {
launch(args);
}
#Override
public void start(final Stage stage) {
final Scene scene = new Scene(new Group());
TableView<MyData> table = new TableView<>();
table.setPrefHeight(700);
final TableColumn<MyData, Long> nameCol = new TableColumn("So So");
nameCol.setMinWidth(200);
nameCol.setCellValueFactory(new PropertyValueFactory<>("i"));
// Allow to display Textflow in Column
nameCol.setCellFactory(new Callback<TableColumn<MyData, Long>, TableCell<MyData, Long>>() {
#Override
public TableCell<MyData, Long> call(TableColumn<MyData, Long> column) {
return new TableCell<MyData, Long>() {
#Override
protected void updateItem(Long item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
setStyle("");
} else {
WebView webview = new WebView();
webview.setPrefWidth(700.0);
WebEngine engine = webview.getEngine();
String textHTML = new String(new char[item.intValue()]).replace("\0", " <b> bold </b> normal, ");
// textHTML = "<body>"
// + textHTML + "</body>";
engine.loadContent(textHTML);
setGraphic(webview);
engine.documentProperty().addListener((obj, prev, newv) -> {
String heightText = engine.executeScript(
"window.getComputedStyle(document.body, null).getPropertyValue('height')"
).toString();
System.out.println("heighttext: " + heightText);
webview.setPrefHeight(Double.parseDouble(heightText.replace("px", "")));
this.setPrefHeight(Double.parseDouble(heightText.replace("px", "")));
setGraphic(webview);
});
}
}
};
}
});
table.setItems(data);
table.getColumns().addAll(nameCol);
((Group) scene.getRoot()).getChildren().addAll(table);
stage.setScene(scene);
stage.show();
}
public static class MyData {
private Long i;
public MyData(Long i) {
this.i = i;
}
public Long getI() {
return i;
}
}
}
Now the outout is
heighttext: 581px
heighttext: 581px
However these values seem to be too big. See screeenshot:

Some progress has been made and cell heights are now calculated more realisticly. Kindly see the relevant the adapted code below.
Relevant changens:
Seems like it is mandatory to call webview.setPrefHeight(-1); before executing the jevascript.
Javascript has been modified. No big change seen, but maybe the result is more general
Open points:
For some reason i still have to add + 15.0 to the calculated height. This is a hack. Seems like some additional lenght has to be considered somewhere.
Functionality on recalculation after resize of column has is not optimal. Using table.refresh() causes significant delay in rendering.
public class TableViewSampleHTML extends Application {
private final ObservableList<MyData> data = FXCollections.observableArrayList(new MyData(1L), new MyData(14L), new MyData(2L), new MyData(3L), new MyData(15L));
public static void main(final String[] args) {
launch(args);
}
#Override
public void start(final Stage stage) {
final Scene scene = new Scene(new Group(), 400, 800);
TableView<MyData> table = new TableView<>();
table.setPrefWidth(400);
table.setPrefHeight(800);
final TableColumn<MyData, Long> nameCol = new TableColumn("So So");
final TableColumn<MyData, Long> col2 = new TableColumn("la la");
nameCol.setPrefWidth(200);
col2.setCellValueFactory(new PropertyValueFactory<>("i"));
nameCol.setCellValueFactory(new PropertyValueFactory<>("i"));
nameCol.widthProperty().addListener((ob,oldV,newV) -> {table.refresh();} );
// Allow to display Textflow in Column
nameCol.setCellFactory(new Callback<TableColumn<MyData, Long>, TableCell<MyData, Long>>() {
#Override
public TableCell<MyData, Long> call(TableColumn<MyData, Long> column) {
return new TableCell<MyData, Long>() {
#Override
protected void updateItem(Long item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
setStyle("");
} else {
WebView webview = new WebView();
WebEngine engine = webview.getEngine();
webview.setPrefHeight(-1); // <- Absolute must at this position (before calling the Javascript)
setGraphic(webview);
String textHTML = new String(new char[item.intValue()]).replace("\0", " <b> bold </b> normal, ");
textHTML = "<body>"
+ textHTML + "</body>";
engine.loadContent(textHTML);
engine.documentProperty().addListener((obj, prev, newv) -> {
String heightText = engine.executeScript( // <- Some modification, which gives moreless the same result than the original
"var body = document.body,"
+ "html = document.documentElement;"
+ "Math.max( body.scrollHeight , body.offsetHeight, "
+ "html.clientHeight, html.scrollHeight , html.offsetHeight );"
).toString();
System.out.println("heighttext: " + heightText);
Double height = Double.parseDouble(heightText.replace("px", "")) + 15.0; // <- Why are this 15.0 required??
webview.setPrefHeight(height);
this.setPrefHeight(height);
});
}
}
};
}
});
table.setItems(data);
table.getColumns().addAll(nameCol);
table.getColumns().addAll(col2);
((Group) scene.getRoot()).getChildren().addAll(table);
stage.setScene(scene);
stage.show();
}
public static class MyData {
private Long i;
public MyData(Long i) {
this.i = i;
}
public Long getI() {
return i;
}
}
}
Output now looks like:

From the example you linked (JavaFX webview, get document height) the height of the document is computed in a ChangeListener on the document:
engine.documentProperty().addListener((prop, oldDoc, newDoc) -> {
String heightText = engine.executeScript(
"window.getComputedStyle(document.body, null).getPropertyValue('height')"
).toString();
System.out.println("heighttext: " + heightText);
});
Output:
heighttext: 36px
heighttext: 581px
heighttext: 581px
In the code in your question you are not executing the height check based upon a ChangeListener. So you are querying the height of the WebView document before the document has been loaded (which is why it is returning zero for your code).

Basedon BerndGirt's answer.
public class WebviewCellFactory<S,T> implements Callback<TableColumn<S,T>, TableCell<S,T>> {
#Override
public TableCell<S, T> call(TableColumn<S, T> column) {
return new TableCell<S, T>() {
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
setStyle("");
} else {
WebView webview = new WebView();
WebEngine engine = webview.getEngine();
webview.setPrefHeight(-1); // <- Absolute must at this position (before calling the Javascript)
webview.setBlendMode(BlendMode.DARKEN);
setGraphic(webview);
engine.loadContent("<body topmargin=0 leftmargin=0 style=\"background-color: transparent;\">"+item+"</body>");
engine.documentProperty().addListener((obj, prev, newv) -> {
String heightText = engine.executeScript( // <- Some modification, which gives moreless the same result than the original
"var body = document.body,"
+ "html = document.documentElement;"
+ "Math.max( body.scrollHeight , body.offsetHeight, "
+ "html.clientHeight, html.scrollHeight , html.offsetHeight );"
).toString();
Double height = Double.parseDouble(heightText.replace("px", "")) + 10 ; // <- Why are this 15.0 required??
webview.setPrefHeight(height);
this.setPrefHeight(height);
});
}
}
};
}
}
then you just need to set
tableColumn.setCellFactory(new WebviewCellFactory());
if there are any mistakes please tell me.

Related

When i run my program my thread is working but images are not setting for the grid can someone give a solution for this.

ISymbol interface
package main;
import javafx.scene.image.Image;
public interface ISymbol {
void setImage(String location,String name);
Image getImage();
void setValue(int value);
int getValue();
}
Symbol class
package main;
import javafx.scene.image.Image;
import java.io.File;
public class Symbol implements ISymbol {
Image image;
int value;
#Override
public void setImage(String location,String name) {
File file = new File(location);
image = new Image(file.toURI().toString(),100,100,true,true);
}
#Override
public Image getImage() {
return image;
}
#Override
public void setValue(int value) {
this.value = value;
}
#Override
public int getValue() {
return value;
}
}
In here i'm trying to add images randomly to a array and i'm using that array in my main class to add those images to my reels
Reel class
package main;
import java.util.Random;
public class Reel {
public Symbol[] spin(){
Symbol cherry = new Symbol();
Symbol redSeven = new Symbol();
Symbol watermelon = new Symbol();
Symbol bell = new Symbol();
Symbol lemon = new Symbol();
Symbol plum = new Symbol();
Random random = new Random();
Symbol[] symbolArray = new Symbol[6];
for (int i = 0; i < symbolArray.length; i++) {
int randomNumber = random.nextInt(6);
System.out.println(randomNumber);
switch (randomNumber) {
case 0:
cherry.setValue(2);
cherry.setImage("/images/cherry.png","cherry");
symbolArray[i] = cherry;
break;
case 1:
lemon.setValue(3);
lemon.setImage("/images/lemon.png","lemon");
symbolArray[i] = lemon;
break;
case 2:
plum.setValue(4);
plum.setImage("/images/plum.png","plum");
symbolArray[i] = plum;
break;
case 3:
watermelon.setValue(5);
watermelon.setImage("/images/watermelon.png", "watermelon");
symbolArray[i] = watermelon;
break;
case 4:
bell.setValue(6);
bell.setImage("/images/bell.png", "bell");
symbolArray[i] = bell;
break;
case 5:
redSeven.setValue(7);
redSeven.setImage("images/redseven.png","seven");
symbolArray[i] = redSeven;
break;
default:
break;
}
}
return symbolArray;
}
}
This is my main class that include all methods. In the btnSpin method i'm calling my thread and for setting images for the reels i have used a reel method
I have debug my program and checked whether the image is coming the image was on there but when i set my image to the image view it wont work while i'm running my thread those imageviews are disappeared can someone give me a solution waiting for a reply thank you :)
SlotMachine class
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.effect.Reflection;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import main.Reel;
import main.Symbol;
public class SlotMachine extends Application implements Runnable {
//creating a thread
Thread thread1 = new Thread(){
#Override public void run(){
reel1();
}
};
//default image for reel
private Image image = new Image("/images/icon.png");
//UI variables
private Text title;
private Label lblStatus,lblInformationArea, lblBetAmount, lblCreditArea;
private ImageView image1, image2, image3;
private Button btnSpin, btnAddCoin, btnBetOne, btnBetMax, btnReset, btnStatistics;
//calculation variables
private int remainingCoins = 10;
private int betAmount, wins, lost, reel1value, reel2value, reel3value;
#Override
public void start(Stage primaryStage) {
GridPane grid = new GridPane();
grid.setPadding(new Insets(10, 0, 10, 0));
grid.setHgap(20);
grid.setVgap(20);
grid.setGridLinesVisible(true);
// Title in row 0 column 3 with styling
title = new Text();
title.setCache(true);
title.setText("REEL RUSH");
title.setFill(Color.YELLOW);
title.setFont(Font.font("Arial", FontWeight.BOLD, 60));
Reflection r = new Reflection();
title.setEffect(r);
GridPane.setConstraints(title, 3, 1);
GridPane.setHalignment(title, HPos.CENTER);
// Reel1 in row 4 column 2
image1 = new ImageView(image);
GridPane.setConstraints(image1, 2, 4);
GridPane.setHalignment(image1, HPos.CENTER);
// Reel2 in row 4 column 3
image2 = new ImageView(image);
GridPane.setConstraints(image2, 3, 4);
GridPane.setHalignment(image2, HPos.CENTER);
// Reel3 in row 4 column 4
image3 = new ImageView(image);
GridPane.setConstraints(image3, 4, 4);
GridPane.setHalignment(image3, HPos.CENTER);
// adding mouse click event for image views
image1.setOnMouseClicked(new EventHandler<javafx.scene.input.MouseEvent>() {
#Override
public void handle(javafx.scene.input.MouseEvent event) {
symbolClicked(event);
System.out.println("REEL 1 IS CLICKED");
}
});
image2.setOnMouseClicked(new EventHandler<javafx.scene.input.MouseEvent>() {
#Override
public void handle(javafx.scene.input.MouseEvent event) {
symbolClicked(event);
System.out.println("REEL 2 IS CLICKED");
}
});
image3.setOnMouseClicked(new EventHandler<javafx.scene.input.MouseEvent>() {
#Override
public void handle(javafx.scene.input.MouseEvent event) {
symbolClicked(event);
System.out.println("REEL 3 IS CLICKED");
}
});
// Status label row 8 column 4
lblStatus = new Label("YOU LOOSE");
lblStatus.setId("label-lblStatus");
GridPane.setConstraints(lblStatus, 3, 8);
GridPane.setHalignment(lblStatus, HPos.CENTER);
//information area label row 9 column 3
lblInformationArea = new Label("INFORMATION AREA ");
lblInformationArea.setId("label-lbl");
GridPane.setConstraints(lblInformationArea, 3, 9);
GridPane.setHalignment(lblInformationArea, HPos.CENTER);
// Credit area label row 5 column 2
lblCreditArea = new Label("CREDIT AREA: " + remainingCoins);
lblCreditArea.setId("label-lbl");
GridPane.setConstraints(lblCreditArea, 2, 9);
GridPane.setHalignment(lblCreditArea, HPos.CENTER);
// Bet amount label row 5 column 4
lblBetAmount = new Label("BET AMOUNT: " +betAmount);
lblBetAmount.setId("label-lbl");
GridPane.setConstraints(lblBetAmount, 4, 9);
GridPane.setHalignment(lblBetAmount, HPos.CENTER);
// Add coin button row 6 column 3
btnSpin = new Button("SPIN");
btnSpin.setId("button-btnSpin");
GridPane.setConstraints(btnSpin, 3, 10);
GridPane.setHalignment(btnSpin, HPos.CENTER);
// Add coin button row 8 column 1
btnAddCoin = new Button("ADD COIN");
GridPane.setConstraints(btnAddCoin, 2, 12);
GridPane.setHalignment(btnAddCoin, HPos.CENTER);
// Add coin button row 8 column 2
btnBetOne = new Button("BET ONE");
btnBetOne.setFont(Font.font("Arial", 20));
GridPane.setConstraints(btnBetOne, 1, 12);
GridPane.setHalignment(btnBetOne, HPos.CENTER);
// Add coin button row 8 column 3
btnBetMax = new Button("BET MAX");
GridPane.setConstraints(btnBetMax, 4, 12);
GridPane.setHalignment(btnBetMax, HPos.CENTER);
// Add coin button row 8 column 4
btnReset = new Button("RESET");
GridPane.setConstraints(btnReset, 6, 12);
GridPane.setHalignment(btnReset, HPos.CENTER);
// Add coin button row 8 column 5
btnStatistics = new Button("STATISTICS");
GridPane.setConstraints(btnStatistics, 3, 12);
GridPane.setHalignment(btnStatistics, HPos.CENTER);
// ------------------- Adding mouse events for each button ---------------------------
btnAddCoin.setOnAction(new EventHandler<javafx.event.ActionEvent>() {
#Override
public void handle(javafx.event.ActionEvent event) {
remainingCoins++;
lblCreditArea.setText("CREDIT AREA: "+remainingCoins);
}
});
btnBetOne.setOnAction(new EventHandler<javafx.event.ActionEvent>() {
#Override
public void handle(javafx.event.ActionEvent event) {
if (remainingCoins > 0) {
remainingCoins--;
betAmount++;
lblBetAmount.setText("BET AMOUNT: " + betAmount);
lblCreditArea.setText("CREDIT AREA: " + remainingCoins);
} else {
lblInformationArea.setText("No Credits Left!!!! Please Insert A Coin");
}
}
});
btnSpin.setOnAction(new EventHandler<javafx.event.ActionEvent>() {
#Override
public void handle(javafx.event.ActionEvent event) {
if (betAmount > 0) {
System.out.println("SPIN BUTTON CLICKED");
thread1.start();
} else {
lblInformationArea.setText("You did not bet!!!! Please Bet");
}
}
});
btnReset.setOnAction(new EventHandler<javafx.event.ActionEvent>() {
#Override
public void handle(javafx.event.ActionEvent event) {
remainingCoins = 10;
betAmount = 0;
lblBetAmount.setText("BET AMOUNT: " + betAmount);
lblCreditArea.setText("CREDIT AREA: " + remainingCoins);
lblInformationArea.setText("Status");
image1.setImage(image);
image2.setImage(image);
image3.setImage(image);
}
});
btnBetMax.setOnAction(new EventHandler<javafx.event.ActionEvent>() {
#Override
public void handle(javafx.event.ActionEvent event) {
if (remainingCoins >= 3) {
remainingCoins = remainingCoins - 3;
betAmount = betAmount + 3;
lblBetAmount.setText("BET AMOUNT: " + betAmount);
lblCreditArea.setText("CREDIT AREA: " + remainingCoins);
} else {
lblInformationArea.setText("No Credits Left!!!! Please Insert A Coin");
}
}
});
btnStatistics.setOnAction(new EventHandler<javafx.event.ActionEvent>() {
#Override
public void handle(javafx.event.ActionEvent event) {
//statistic();
lblInformationArea.setText("Spin the Reel First");
}
});
// adding all to the scene
grid.getChildren().addAll(title, lblStatus, lblInformationArea, lblCreditArea, lblBetAmount, btnAddCoin, btnBetMax, btnBetOne, btnReset, btnSpin, btnStatistics, image1, image3 , image2);
grid.setAlignment(Pos.TOP_CENTER);
Scene scene = new Scene(grid, 1450, 920);
scene.getStylesheets().add("/css/main.css");
primaryStage.setTitle("REEL RUSH");
primaryStage.setScene(scene);
primaryStage.show();
}
public void reel1() {
while (true) {
//creating reel objects for each reel
Reel firstReel = new Reel();
Reel secondReel = new Reel();
Reel thirdReel = new Reel();
Symbol[] firstReelSymbols = firstReel.spin();
Symbol[] secondReelSymbols = secondReel.spin();
Symbol[] thirdReelSymbols = thirdReel.spin();
for (Symbol item : firstReelSymbols) {
Image img1 = item.getImage();
image1.setImage(img1);
reel1value = item.getValue();
}
for (Symbol item : secondReelSymbols) {
Image img1 = item.getImage();
image2.setImage(img1);
reel1value = item.getValue();
}
for (Symbol item : thirdReelSymbols) {
Image img1 = item.getImage();
image3.setImage(img1);
reel1value = item.getValue();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void symbolClicked(javafx.scene.input.MouseEvent event) {
//TODO stop thread when image clicked
if((reel1value==reel3value)&&(reel2value==reel3value)){
//check if all 3 numbers are same
lblInformationArea.setText("You Win");
remainingCoins+=(betAmount*reel1value);
lblCreditArea.setText("Credits Area: "+remainingCoins);
wins++;
}else{
lblInformationArea.setText("You Loose");
lost++;
}
betAmount=0;
lblBetAmount.setText("Bet Amount: "+betAmount);
}
#Override
public void run() {
}
public static void main(String[] args){
launch(args);
}
}
Technically, while you can "run" an Application, we would never implement Runnable in an Application. JavaFX will do its own application management, and there is no way you can "run" an application in another thread.
Now, back to your question. Your reel1() is badly written. If you know a particular method is going to run in non-UI thread (i.e. JavaFX Application thread), you must take note not to directly set any kind of value that changes the UI within it.
So, this:
for (Symbol item : firstReelSymbols) {
Image img1 = item.getImage();
image1.setImage(img1);
reel1value = item.getValue();
}
should becomes something like:
for (Symbol item : firstReelSymbols) {
Image img1 = item.getImage();
Platform.runLater(() -> image1.setImage(img1)); // Run this run on UI thread
reel1value = item.getValue(); // Not sure what this value is for, may need to be wrapped inside Platform.runLater() if it affects UI
}
Other than this, it is weird that you are looping through a list of Symbol objects, and inside the loop you are setting the same image1 field.

HTMLEditor subscript and superscript text

I have been trying to show subscript and superscript text in HTMLEditor. there are two buttons for sub and sup mode. the user types the (sub/sup)text in a textfield and press the OK button which allows the textfield text to be rendered as sub or sup in HTMLEditor. The code is as follows:
import java.util.List;
import java.util.regex.Pattern;
import javafx.application.*;
import javafx.collections.FXCollections;
import javafx.event.*;
import javafx.geometry.Orientation;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.web.HTMLEditor;
import javafx.stage.Stage;
public class HTMLEditorCustomizationSample extends Application {
// limits the fonts a user can select from in the html editor.
private static final List<String> limitedFonts = FXCollections.observableArrayList("Arial", "Times New Roman", "Courier New", "Comic Sans MS");
String sup = " ⁺⁻⁼⁽⁾⁰¹²³⁴⁵⁶⁷⁸⁹ᴬᵃᴭᵆᵄᵅᶛᴮᵇᶜᶝᴰᵈᶞᴱᵉᴲᵊᵋᶟᵌᶠᴳᵍᶢˠʰᴴʱᴵⁱᶦᶤᶧᶥʲᴶᶨᶡᴷᵏˡᴸᶫᶪᶩᴹᵐᶬᴺⁿᶰᶮᶯᵑᴼᵒᵓᵔᵕᶱᴽᴾᵖᶲʳᴿʴʵʶˢᶳᶴᵀᵗᶵᵁᵘᶸᵙᶶᶣᵚᶭᶷᵛⱽᶹᶺʷᵂˣʸᶻᶼᶽᶾꝰᵜᵝᵞᵟᶿᵠᵡᵸჼˤⵯ";
String supchars = " +−=()0123456789AaÆᴂɐɑɒBbcɕDdðEeƎəɛɜɜfGgɡɣhHɦIiɪɨᵻɩjJʝɟKklLʟᶅɭMmɱNnɴɲɳŋOoɔᴖᴗɵȢPpɸrRɹɻʁsʂʃTtƫUuᴜᴝʉɥɯɰʊvVʋʌwWxyzʐʑʒꝯᴥβγδθφχнნʕⵡ";
String subchars=" +−=()0123456789aeəhijklmnoprstuvxβγρφχ";
String sub=" ₊₋₌₍₎₀₁₂₃₄₅₆₇₈₉ₐₑₔₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓᵦᵧᵨᵩᵪ";
char[] csup = sup.toCharArray();
char[] characters = supchars.toCharArray();
char[] csub = sub.toCharArray();
char[] character = subchars.toCharArray();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
// create a new html editor and show it before we start modifying it.
final HTMLEditor htmlEditor = new HTMLEditor();
stage.setScene(new Scene(htmlEditor));
stage.show();
// hide controls we don't need.
hideImageNodesMatching(htmlEditor, Pattern.compile(".*(Cut|Copy|Paste).*"), 0);
Node seperator = htmlEditor.lookup(".separator");
seperator.setVisible(false);
seperator.setManaged(false);
// modify font selections.
int i = 0;
for (Node candidate : (htmlEditor.lookupAll("MenuButton"))) {
// fonts are selected by the second menu in the htmlEditor.
if (candidate instanceof MenuButton && i == 1) {
// limit the font selections to our predefined list.
MenuButton menuButton = (MenuButton) candidate;
List<MenuItem> removalList = FXCollections.observableArrayList();
final List<MenuItem> fontSelections = menuButton.getItems();
for (MenuItem item : fontSelections) {
if (!limitedFonts.contains(item.getText())) {
removalList.add(item);
}
}
fontSelections.removeAll(removalList);
// Select a font from out limited font selection.
// Selection done in Platform.runLater because if you try to do
// the selection immediately, it won't take place.
Platform.runLater(new Runnable() {
#Override
public void run() {
boolean fontSelected = false;
for (final MenuItem item : fontSelections) {
if ("Comic Sans MS".equals(item.getText())) {
if (item instanceof RadioMenuItem) {
((RadioMenuItem) item).setSelected(true);
fontSelected = true;
}
}
}
if (!fontSelected && fontSelections.size() > 0 && fontSelections.get(0) instanceof RadioMenuItem) {
((RadioMenuItem) fontSelections.get(0)).setSelected(true);
}
}
});
}
i++;
}
// add a custom button to the top toolbar.
Node node = htmlEditor.lookup(".top-toolbar");
if (node instanceof ToolBar) {
ToolBar bar = (ToolBar) node;
ToggleButton supButton = new ToggleButton("x²");
ToggleButton subButton = new ToggleButton("x₂");
TextField txt = new TextField();
Button okBtn = new Button("OK");
Button clrBtn = new Button("CLEAR");
ToggleGroup group = new ToggleGroup();
supButton.setToggleGroup(group);
subButton.setToggleGroup(group);
Separator v1=new Separator();
v1.setOrientation(Orientation.VERTICAL);
Separator v2=new Separator();
v2.setOrientation(Orientation.VERTICAL);
txt.setDisable(true);
okBtn.setDisable(true);;
clrBtn.setDisable(true);
bar.getItems().add(v1);
bar.getItems().add(supButton);
bar.getItems().add(subButton);
bar.getItems().add(v2);
bar.getItems().add(txt);
bar.getItems().add(okBtn);
bar.getItems().add(clrBtn);
okBtn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent arg0) {
System.out.println(htmlEditor.getHtmlText());
if (supButton.isSelected()) {
txt.setPromptText(" Enter the superscript text ");
String text = htmlEditor.getHtmlText().replaceAll("</p></body></html>", "");
text = text.replaceAll("<html dir=\"ltr\"><head></head><body contenteditable=\"true\"><p>", "");
System.out.println(text);
text="<p>"+text + "<sup>"+ txt.getText()+"</sup></p>";
System.out.println(text);
htmlEditor.setHtmlText(text);
}
else if (subButton.isSelected()) {
txt.setPromptText(" Enter the superscript text ");
String text = htmlEditor.getHtmlText().replaceAll("</p></body></html>", "");
text = text.replaceAll("<html dir=\"ltr\"><head></head><body contenteditable=\"true\"><p>", "");
System.out.println(text);
text=text + "<sub>"+ txt.getText()+"</sup></p>";
System.out.println(text);
htmlEditor.setHtmlText(text);
}
}
});
clrBtn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent arg0) {
txt.clear();
}
});
supButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent arg0) {
if (supButton.isSelected()) {
txt.setPromptText(" Enter the superscript text ");
txt.setDisable(false);
okBtn.setDisable(false);;
clrBtn.setDisable(false);
}
}
});
subButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent arg0) {
if (subButton.isSelected()) {
txt.setPromptText(" Enter the subscript text ");
txt.setDisable(false);
okBtn.setDisable(false);;
clrBtn.setDisable(false);
}
}
});
}
}
private String convertSupText(String dsup) {
char[] cdsup = dsup.toCharArray();
String data="";
for (int i = 0; i < cdsup.length; i++) {
for (int j = 0; j < characters.length; j++) {
if (cdsup[i] == characters[j]) {
data = data + csup[j];
}
}
}
return data;
}
private String convertSubText(String dsup) {
char[] cdsup = dsup.toCharArray();
String data="";
for (int i = 0; i < cdsup.length; i++) {
for (int j = 0; j < character.length; j++) {
if (cdsup[i] == character[j]) {
data = data + csub[j];
}
}
}
return data;
}
// hide buttons containing nodes whose image url matches a given name pattern.
public void hideImageNodesMatching(Node node, Pattern imageNamePattern, int depth) {
if (node instanceof ImageView) {
ImageView imageView = (ImageView) node;
String url = imageView.getImage().impl_getUrl();
if (url != null && imageNamePattern.matcher(url).matches()) {
Node button = imageView.getParent().getParent();
button.setVisible(false);
button.setManaged(false);
}
}
if (node instanceof Parent) {
for (Node child : ((Parent) node).getChildrenUnmodifiable()) {
hideImageNodesMatching(child, imageNamePattern, depth + 1);
}
}
}
}
The problem is that after adding the subscript or superscript text, the cursor still remains in subscript or superscript mode and every time the text is added it goes on a newline.
#Manoj I think your problem is that you don't know what the HTMLeditor is doing with any text you enter in the textfield (aka WebPage). Appearantly it is applying the your <sub> tag to the next text you enter (adding 1 and typing a normal 2 afterwards results in 12):
<html dir="ltr"><head></head><body contenteditable="true"><p><br><sup>1</sup></p></body></html>
<html dir="ltr"><head></head><body contenteditable="true"><p><br><sup>1<font size="2">1</font></sup></p></body></html>
I looked into the files (HTMLEditor>HTMLEditorSkin>WebPage>twkExecuteCommand) and in the end commands like bold/italic are executed in a dll (jfxwebkit). My knowledge is exceeded here. I see no solution which would not involve rewriting the whole HTMLEditor + native libraries.
(just included this in an answer bc comment length was exceeded)
thought so. I have done a work around using webview along with html editor. And it works fine for now. The code is as follows:
import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Separator;
import javafx.scene.control.Button;
import javafx.scene.control.ToolBar;
import javafx.scene.control.Tooltip;
import javafx.scene.web.HTMLEditor;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
public class FXMLDocumentController implements Initializable {
#FXML
private HTMLEditor HE;
#FXML
private WebView WV;
WebEngine webEngine;
Button supButton;
Button subButton;
Tooltip sup;
Tooltip sub;
Alert info= new Alert(Alert.AlertType.INFORMATION);;
#Override
public void initialize(URL url, ResourceBundle rb) {
// TODO
webEngine = WV.getEngine();
supButton = new Button("x²");
subButton = new Button("x₂");
supButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent arg0) {
info.setTitle("SUCCESS");
info.setHeaderText("Information");
info.setContentText("Use <sup>Text to to superscripted</sup> to use superscript fuction.\n Press Preview button to preview the changes");
info.showAndWait();
}});
subButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent arg0) {
info.setTitle("SUCCESS");
info.setHeaderText("Information");
info.setContentText("Use <sub>Text to to subscripted</sub> to use subscript fuction.\n Press Preview button to preview the changes");
info.showAndWait();
}});
sup = new Tooltip();
sub = new Tooltip();
sup.setText(" Use <sup>Text to to superscripted</sup> to use superscript fuction.\n Press Preview button to preview the changes ");
sub.setText(" Use <sub>Text to to subscripted</sub> to use subscript fuction.\n Press Preview button to preview the changes ");
Node node = HE.lookup(".top-toolbar");
if (node instanceof ToolBar) {
ToolBar bar = (ToolBar) node;
Separator v2 = new Separator();
v2.setOrientation(Orientation.VERTICAL);
bar.getItems().add(supButton);
bar.getItems().add(subButton);
bar.getItems().add(v2);
}
supButton.setTooltip(sup);
subButton.setTooltip(sub);
}
#FXML
private void handleKeyTyped(ActionEvent event) {
String text = HE.getHtmlText();
text = text.replaceAll("<sup>", "<sup>");
text = text.replaceAll("</sup>", "</sup>");
text = text.replaceAll("<sub>", "<sub>");
text = text.replaceAll("</sub>", "</sub>");
webEngine.loadContent(text);
}
}

UndoFX: Undo/Redo recording every pixel of drag

I'm using UndoFX & ReactFX for implementing the Undo/Redo function for my 2D shape application.
The problem is when i move my shape the EventStream records every X/Y pixel of movement. I just want to record the last position (when the user releases the drag).
What i have tried so far:
Instead of using changesOf(rect.xProperty()).map(c -> new xChange(c)); and
changesOf(rect.yProperty()).map(c -> new yChange(c));
I created a DoubleProperty x,y, and saved the shape x,y Property to these variables when the user mouse is released.
Lastly i change the changesOf to: changesOf(this.x).map(c -> new xChange(c)); and changesOf(this.y).map(c -> new yChange(c));
But that did not work, it behaved just like before.
....
private class xChange extends RectangleChange<Double> {
public xChange(Double oldValue, Double newValue) {
super(oldValue, newValue);
}
public xChange(Change<Number> c) {
super(c.getOldValue().doubleValue(), c.getNewValue().doubleValue());
}
#Override void redo() { rect.setX(newValue); }
#Override xChange invert() { return new xChange(newValue, oldValue); }
#Override Optional<RectangleChange<?>> mergeWith(RectangleChange<?> other) {
if(other instanceof xChange) {
return Optional.of(new xChange(oldValue, ((xChange) other).newValue));
} else {
return Optional.empty();
}
}
#Override
public boolean equals(Object other) {
if(other instanceof xChange) {
xChange that = (xChange) other;
return Objects.equals(this.oldValue, that.oldValue)
&& Objects.equals(this.newValue, that.newValue);
} else {
return false;
}
}
}
...
EventStream<xChange> xChanges = changesOf(rect.xProperty()).map(c -> new xChange(c));
EventStream<yChange> yChanges = changesOf(rect.yProperty()).map(c -> new yChange(c));
changes = merge(widthChanges, heightChanges, xChanges, yChanges);
undoManager = UndoManagerFactory.unlimitedHistoryUndoManager(
changes, // stream of changes to observe
c -> c.invert(), // function to invert a change
c -> c.redo(), // function to undo a change
(c1, c2) -> c1.mergeWith(c2)); // function to merge two changes
You need to merge the changes in x with the changes in y. At present, a change in x followed by a change in y cannot be merged, so if you move the shape so that it alternates x and y changes (e.g. moving it diagonally), then each individual change will not merge with the previous one.
One way to do this is to generate changes whose old and new values are the locations, e.g. represented by Point2D objects. Here's a quick example:
import java.util.Objects;
import java.util.Optional;
import org.fxmisc.undo.UndoManager;
import org.fxmisc.undo.UndoManagerFactory;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.SuspendableEventStream;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class UndoRectangle extends Application {
#Override
public void start(Stage primaryStage) {
Rectangle rect = new Rectangle(50, 50, 150, 100);
rect.setFill(Color.CORNFLOWERBLUE);
EventStream<PositionChange> xChanges = EventStreams.changesOf(rect.xProperty()).map(c -> {
double oldX = c.getOldValue().doubleValue();
double newX = c.getNewValue().doubleValue();
double y = rect.getY();
return new PositionChange(new Point2D(oldX, y), new Point2D(newX, y));
});
EventStream<PositionChange> yChanges = EventStreams.changesOf(rect.yProperty()).map(c -> {
double oldY = c.getOldValue().doubleValue();
double newY = c.getNewValue().doubleValue();
double x = rect.getX();
return new PositionChange(new Point2D(x, oldY), new Point2D(x, newY));
});
SuspendableEventStream<PositionChange> posChanges = EventStreams.merge(xChanges, yChanges)
.reducible(PositionChange::merge);
UndoManager undoManager = UndoManagerFactory.unlimitedHistoryUndoManager(posChanges,
PositionChange::invert,
c -> posChanges.suspendWhile(() -> {
rect.setX(c.getNewPosition().getX());
rect.setY(c.getNewPosition().getY());
}),
(c1, c2) -> Optional.of(c1.merge(c2))
);
class MouseLoc { double x, y ; }
MouseLoc mouseLoc = new MouseLoc();
rect.setOnMousePressed(e -> {
mouseLoc.x = e.getSceneX();
mouseLoc.y = e.getSceneY();
});
rect.setOnMouseDragged(e -> {
rect.setX(rect.getX() + e.getSceneX() - mouseLoc.x);
rect.setY(rect.getY() + e.getSceneY() - mouseLoc.y);
mouseLoc.x = e.getSceneX();
mouseLoc.y = e.getSceneY();
});
rect.setOnMouseReleased(e -> undoManager.preventMerge());
Pane pane = new Pane(rect);
Button undo = new Button("Undo");
undo.disableProperty().bind(Bindings.not(undoManager.undoAvailableProperty()));
undo.setOnAction(e -> undoManager.undo());
Button redo = new Button("Redo");
redo.disableProperty().bind(Bindings.not(undoManager.redoAvailableProperty()));
redo.setOnAction(e -> undoManager.redo());
HBox buttons = new HBox(5, undo, redo);
buttons.setAlignment(Pos.CENTER);
BorderPane.setMargin(buttons, new Insets(5));
BorderPane root = new BorderPane(pane, null, null, buttons, null);
Scene scene = new Scene(root, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class PositionChange {
private final Point2D oldPosition ;
private final Point2D newPosition ;
public PositionChange(Point2D oldPos, Point2D newPos) {
this.oldPosition = oldPos ;
this.newPosition = newPos ;
}
public Point2D getOldPosition() {
return oldPosition;
}
public Point2D getNewPosition() {
return newPosition;
}
public PositionChange merge(PositionChange other) {
return new PositionChange(oldPosition, other.newPosition);
}
public PositionChange invert() {
return new PositionChange(newPosition, oldPosition);
}
#Override
public boolean equals(Object o) {
if (o instanceof PositionChange) {
PositionChange other = (PositionChange) o ;
return Objects.equals(oldPosition, other.oldPosition)
&& Objects.equals(newPosition, other.newPosition);
} else return false ;
}
#Override
public int hashCode() {
return Objects.hash(oldPosition, newPosition);
}
}
public static void main(String[] args) {
launch(args);
}
}
Note that it's important the "undo" is implemented as an "atomic" change, so the undo manager sees (and ignores) a single change when you implement the undo. This can be achieved by suspending the event stream during the undo.

How to limit the amount of characters a javafx textfield

I´m using a FXML to set my form, but I need to set the limit of characters in textfields. How can I made this ?
You can't directly set a limit to number of characters. But you can add a listener to lengthProperty() of the textfield
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TextFieldLimit extends Application {
private static final int LIMIT = 10;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(final Stage primaryStage) {
final TextField textField = new TextField();
textField.lengthProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable,
Number oldValue, Number newValue) {
if (newValue.intValue() > oldValue.intValue()) {
// Check if the new character is greater than LIMIT
if (textField.getText().length() >= LIMIT) {
// if it's 11th character then just setText to previous
// one
textField.setText(textField.getText().substring(0, LIMIT));
}
}
}
});
VBox vbox = new VBox(20);
vbox.getChildren().add(textField);
Scene scene = new Scene(vbox, 400, 300);
primaryStage.setScene(scene);
primaryStage.show();
}
}
One more elegance solution
Pattern pattern = Pattern.compile(".{0,25}");
TextFormatter formatter = new TextFormatter((UnaryOperator<TextFormatter.Change>) change -> {
return pattern.matcher(change.getControlNewText()).matches() ? change : null;
});
textField.setTextFormatter(formatter);
where 0 and 25 - min and max amount of chars. + ability to set a pattern of input text
Here is my solution to limit the length of a textfield.
I would not recommend solutions which use a listener (on text property or on length property), they do not behave correctly in all situations (for what I have seen).
I create an HTML input text with a max length, and compare it to my textfield in JavaFX. I had the same behavior with paste operations (Ctrl + V), cancel operations (Ctrl + Z) in both cases. The goal here is to check if the text is valid BEFORE modifying the textfield.
We could use a similar approach for a numeric text field.
import java.util.Objects;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.control.TextField;
public class LimitedTextField extends TextField {
private final IntegerProperty maxLength;
public LimitedTextField() {
super();
this.maxLength = new SimpleIntegerProperty(-1);
}
public IntegerProperty maxLengthProperty() {
return this.maxLength;
}
public final Integer getMaxLength() {
return this.maxLength.getValue();
}
public final void setMaxLength(Integer maxLength) {
Objects.requireNonNull(maxLength, "Max length cannot be null, -1 for no limit");
this.maxLength.setValue(maxLength);
}
#Override
public void replaceText(int start, int end, String insertedText) {
if (this.getMaxLength() <= 0) {
// Default behavior, in case of no max length
super.replaceText(start, end, insertedText);
}
else {
// Get the text in the textfield, before the user enters something
String currentText = this.getText() == null ? "" : this.getText();
// Compute the text that should normally be in the textfield now
String finalText = currentText.substring(0, start) + insertedText + currentText.substring(end);
// If the max length is not excedeed
int numberOfexceedingCharacters = finalText.length() - this.getMaxLength();
if (numberOfexceedingCharacters <= 0) {
// Normal behavior
super.replaceText(start, end, insertedText);
}
else {
// Otherwise, cut the the text that was going to be inserted
String cutInsertedText = insertedText.substring(
0,
insertedText.length() - numberOfexceedingCharacters
);
// And replace this text
super.replaceText(start, end, cutInsertedText);
}
}
}
}
Tested with JavaFX 8 and Java 8u45
This is a very simple solution that seems to work for me.
textfield.setOnKeyTyped(event ->{
int maxCharacters = 5;
if(tfInput.getText().length() > maxCharacters) event.consume();
});
I use a simple call to ChangeListener, where I test the condition to perform stops.
textFild.addListener((observable, oldValue, newValue) -> {
if (newValue.length() == MAX_SIZE) {
textField.setText(oldValue);
}
});
This is a better way to do the job on a generic text field:
public static void addTextLimiter(final TextField tf, final int maxLength) {
tf.textProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(final ObservableValue<? extends String> ov, final String oldValue, final String newValue) {
if (tf.getText().length() > maxLength) {
String s = tf.getText().substring(0, maxLength);
tf.setText(s);
}
}
});
}
Works perfectly, except for that Undo bug.
the following 1-liner will exactly do it, wheras 5 is the limit of the TextField tf:
tf.setTextFormatter(new TextFormatter<>(c -> c.getControlNewText().matches(".{0,5}") ? c : null));
This is a solution that works well:
#FXML
void limitTextFields(KeyEvent event) {
int maxLength = 5;
TextField tf = (TextField) event.getSource();
if (tf.getText().length() > maxLength) {
tf.deletePreviousChar();
}
}

Java "Could Not Serialize the Data"

I'm trying to get my clipboard to receive some custom data in a drag and drop. The custom data is another java type. This other type does implement serializable, so I'm really not sure why this isn't working. Any ideas are appreciated!
imgView.setOnDragDetected(new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
ClipboardContent content = new ClipboardContent();
content.put(dataFormat, RHSIconizedToken.this);
Dragboard db = imgView.startDragAndDrop(TransferMode.ANY);
db.setContent(content);
event.consume();
}
});
To retrieve this object later I'm using:
RHSIconizedToken replacementRHSiToken = (RHSIconizedToken) db.getContent(RHSIconizedToken.getDataFormat());
I'm getting the following error, but the RHSIconizedToken does implement Serializable
java.lang.IllegalArgumentException: Could not serialize the data
GetDataFormat returns the DataFormat Object that is used in the put argument in the first code example.
That's because your object is not serializable.
Indeed, it's not because it implements Serializable that it is Serializable.
Look deeper inside the exception, you might find something like this
Caused by: java.io.NotSerializableException: javafx.beans.property.SimpleObjectProperty
Maybe making some fields transient will help.
If your drag object isn't serializable, save it in a global variable during the drag. Here's a JavaFx (Java8 with lambdas) example with draggable tabs that go bewteen panes within the same JVM.
import java.util.Random;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class DraggingTabPane extends Application {
private static final DataFormat TAB_TYPE = new DataFormat("nonserializableObject/tab");
private static Tab dndTab;// global for drag-n-drop of non-serializable type
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
TabPane tabPane1 = createDndTabPane();
TabPane tabPane2 = createDndTabPane();
VBox root = new VBox(10);
root.getChildren().addAll(tabPane1, tabPane2);
final Random rng = new Random();
for (int i=1; i<=8; i++) {
final Tab tab = createDraggableTab("Tab "+i);
final StackPane pane = new StackPane();
int red = rng.nextInt(256);
int green = rng.nextInt(256);
int blue = rng.nextInt(256);
String style = String.format("-fx-background-color: rgb(%d, %d, %d);", red, green, blue);
pane.setStyle(style);
final Label label = new Label("This is tab "+i);
label.setStyle(String.format("-fx-text-fill: rgb(%d, %d, %d);", 256-red, 256-green, 256-blue));
pane.getChildren().add(label);
pane.setMinWidth(600);
pane.setMinHeight(250);
tab.setContent(pane);
if (i<=4) {
tabPane1.getTabs().add(tab);
} else {
tabPane2.getTabs().add(tab);
}
}
primaryStage.setScene(new Scene(root, 600, 600));
primaryStage.show();
}
public TabPane createDndTabPane() {
final TabPane tabPane = new TabPane();
tabPane.setOnDragOver(event -> {
if (event.getDragboard().hasContent(TAB_TYPE)
&& dndTab.getTabPane() != tabPane) {// && different from source location
event.acceptTransferModes(TransferMode.MOVE);
event.consume();
}
});
tabPane.setOnDragDropped(event -> {
if (event.getDragboard().hasContent(TAB_TYPE)
&& dndTab.getTabPane() != tabPane) {// && different from source location
dndTab.getTabPane().getTabs().remove(dndTab);
tabPane.getTabs().add(dndTab);
event.setDropCompleted(true);
event.consume();
}
});
return tabPane;
}
private Tab createDraggableTab(String text) {
final Tab tab = new Tab();
final Label label = new Label(text);
tab.setGraphic(label);
label.setOnDragDetected(event -> {
Dragboard dragboard = label.startDragAndDrop(TransferMode.MOVE);
ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.put(TAB_TYPE, 1);
dndTab = tab;
dragboard.setContent(clipboardContent);
event.consume();
});
return tab ;
}
}

Resources