Double tab change event on tab closing in JavaFX - java

I have the following program:
public class MainClass extends Application {
public static void main(String[] arg) {
launch(arg);
}
#Override
public void start(Stage primaryStage) throws Exception {
TabPane tabPane = new TabPane();
tabPane.getSelectionModel().selectedItemProperty().addListener((ov, oldTab, newTab) -> {
System.out.println("Tab change: " + oldTab + "/" + newTab);
});
Tab tab = new Tab("Test tab");
tab.setOnCloseRequest((event) -> {
System.out.println("Removing tab");
event.consume();
//I need to remove tab manually
tabPane.getTabs().remove(tab);
});
System.out.println("Adding tab");
tabPane.getTabs().add(tab);
Group root = new Group(tabPane);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
}
When I run it and click close icon on Tab I have the following output of the program:
Adding tab
Tab change: null/javafx.scene.control.Tab#70b1aa69
Removing tab
Tab change: javafx.scene.control.Tab#70b1aa69/null
Tab change: null/javafx.scene.control.Tab#70b1aa69
As you see I get two Tab change events when I closing tab but I need only one. How to fix it?

Interesting bug - was puzzled as to why/how the removed tab can still be the selected tab even though no longer in the tabs list.
First question was, where exactly the selection happens: that's done in the mousePressedHandler installed by the TabHeaderSkin
setOnMousePressed(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent me) {
if (getTab().isDisable()) {
return;
}
if (me.getButton().equals(MouseButton.MIDDLE)) {
if (showCloseButton()) {
Tab tab = getTab();
if (behavior.canCloseTab(tab)) {
removeListeners(tab);
behavior.closeTab(tab);
}
}
} else if (me.getButton().equals(MouseButton.PRIMARY)) {
behavior.selectTab(getTab());
}
}
});
But then: how comes that this handler is still active after removal of the tab (and hopefully its visuals as well)? The cleanup of the visual parts is the task of TabPaneSkin, it listens to the tabs list and removes the TabHeaderSkin (aka: the component that shows the tab above its content). But the cleanup is not immediately complete for two reasons:
fade-out animation keeps the header alive until the animation is ready, that's fine
header's internal cleanup (messaged via header.removeListeners) is incomplete, as it removes children and listeners, but fails to remove the mouseHandler - and that's the bug.
Code from TabHeaderSkin:
private void removeListeners(Tab tab) {
listener.dispose();
inner.getChildren().clear();
getChildren().clear();
// following line is missing:
setOnMousePressed(null)
}
A way to hack around is to register our own listener on tabs, and force the handler to null on removal. Note: the listener must be notified after core did its job, so either install in a custom skin or after the tabPane's skin has been set.
To illustrate, I modified your example accordingly:
public class TabPaneRemoveSelected extends Application {
public static void main(String[] arg) {
launch(arg);
}
public static class MyTabSkin extends TabPaneSkin {
public MyTabSkin(TabPane pane) {
super(pane);
pane.getTabs().addListener(this::tabsChanged);
}
protected void tabsChanged(Change<? extends Tab> c) {
while (c.next()) {
if (c.wasRemoved()) {
// lookup all TabHeaderSkins
Set<Node> tabHeaders = getSkinnable().lookupAll(".tab");
tabHeaders.stream()
.filter(p -> p instanceof Parent)
.map(p -> (Parent) p)
.forEach(p -> {
// all children removed indicates being in the process
// of being removed
if (p.getChildrenUnmodifiable().size() == 0) {
// complete removeListeners
p.setOnMousePressed(null);
}
}
);
}
}
}
}
#Override
public void start(Stage primaryStage) throws Exception {
TabPane tabPane = new TabPane() {
#Override
protected Skin<?> createDefaultSkin() {
return new MyTabSkin(this);
}
};
tabPane.getSelectionModel().selectedItemProperty().addListener((ov, oldTab, newTab) -> {
System.out.println("Tab change: " + oldTab + "/" + newTab );
});
Tab tab = new Tab("Test tab");
Tab second = new Tab("second");
installHandler(tabPane, tab, second);
installHandler(tabPane, second);
System.out.println("Adding tab");
tabPane.getTabs().addAll(tab, second);
Group root = new Group(tabPane);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
protected void installHandler(TabPane tabPane, Tab... tab) {
for (Tab tab2 : tab) {
tab2.setOnCloseRequest((event) -> {
System.out.println("Removing tab");
event.consume();
//I need to remove tab manually
tabPane.getTabs().remove(tab2);
});
}
}
}

It seems to be a bug so I opened a bug issue http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8189424 (https://bugs.openjdk.java.net/browse/JDK-8189424) and accept this answer (as soon as SO lets me do it).

Related

Refactor JFoenix JFXDialogLayout alert notification code to a simpler form so that it can be reused for other classes

how can I refactor the following code so that only the code in deleteButton.setOnAction(deleteEvent -> {//only this code varies} changes. Everything else will stay the same but the block of code in the lambda expression varies from time to time when I call the class from another class. The block of code that goes through the lambda expression is supposed to be a void method.
public class A {
public void test() {
// ensure that user can't close the alert
Stage primaryStage = (Stage) RootLayoutController.getRootLayout().getScene().getWindow();
JFXAlert<javafx.scene.control.ButtonType> alert = new JFXAlert<>(primaryStage);
alert.initModality(Modality.APPLICATION_MODAL);
alert.setOverlayClose(false);
//create font awesome icon
String ICON = "\uf071";
Label labelIcon = new Label(ICON);
labelIcon.setStyle("-fx-font-family: 'FontAwesome'; -fx-font-size: 60px; -fx-text-fill: #D34336;");
labelIcon.setPadding(new Insets(0,5,0,0));
// Create the content of the JFXAlert with JFXDialogLayout
JFXDialogLayout layout = new JFXDialogLayout();
Label labelHeading = new Label("Alert Notification");
Label labelBody = new Label("Are you sure you want to delete this?");
layout.setHeading(labelHeading);
layout.setBody(new VBox(new HBox(labelIcon, labelBody)));
// Buttons get added into the actions section of the layout.
JFXButton deleteButton = new JFXButton("Delete");
deleteButton.setDefaultButton(true);
deleteButton.setOnAction(deleteEvent -> {
//only this block of code changes
alert.hideWithAnimation();
});
JFXButton cancelButton = new JFXButton("Cancel");
cancelButton.setCancelButton(true);
cancelButton.setOnAction(closeEvent -> alert.hideWithAnimation());
layout.setActions(deleteButton, cancelButton);
alert.setContent(layout);
alert.showAndWait();
}
}
It is not entirely clear from your question what you are trying to accomplish, but I will take a wild stab at it.
If you are looking to be able to pass a code block to the deleteButton.setOnAction() method, you could use an Interface and pass implementations of that interface to the A class. Then just pass that reference to an internal method for the onAction lambda.
Here is a very quick example of how you could do something like this:
Main.java:
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Action button
Button btnDoSomething = new Button("Do something...");
btnDoSomething.setOnAction(e -> doTheThings(new ImplDoSomething()));
Button btnDoSomethingElse = new Button("Do something else...");
btnDoSomethingElse.setOnAction(e -> doTheThings(new ImplDoSomethingElse()));
VBox mainPane = new VBox(5);
mainPane.setAlignment(Pos.CENTER);
mainPane.setPadding(new Insets(10));
mainPane.getChildren().addAll(btnDoSomething, btnDoSomethingElse);
primaryStage.setScene(new Scene(mainPane));
primaryStage.show();
}
private void doTheThings(IParameterMethod parameterMethod) {
parameterMethod.call();
}
}
The IParameterMethod.java Interface:
public interface IParameterMethod {
void call();
}
Then you can create as many classes as you like that implement that interface, each with their own call() method, allowing you to execute different code.
ImplDoSomething.java
public class ImplDoSomething implements IParameterMethod {
#Override
public void call() {
System.out.println("Doing something!");
}
}
ImplDoSomethingElse.java:
public class ImplDoSomethingElse implements IParameterMethod {
#Override
public void call() {
System.out.println("Doing something else!");
}
}
This should be easily adapted for your project.

JavaFX: how to temporary block the GUI

I'm trying to figure out if blocking the GUI is possible. Basically, my application (which is using the NetBeans Platform and JavaFX) has a connection to the server.
Independently on which screen the user is seeing, if the application loses the connection to the server I'd like to block everything (the users cannot open any new windows or click anywhere) until the application is connected again (it doesn't matter if that needs 5 minutes or 5 hours). Nevertheless, on the top of everything should appear an alert message (always on the top).
The java class which is listening to the server connection doesn't have any reference to JavaFX containers. That's what I actually have:
public class StatusConnectionObserver implements ConnectionObserver {
private final Led led;
private final Label label;
public StatusConnectionObserver(Led led, Label label) {
this.led = led;
this.label = label;
}
#Override
public void setConnected(boolean connected) {
if (connected) {
Platform.runLater(() -> {
led.setLedColor(Color.rgb(59, 249, 53));
label.setText("Connected");
});
} else {
Platform.runLater(() -> {
led.setLedColor(Color.RED);
label.setText("Disconnected");
});
}
}
}
and:
public class ConnectionComponent {
private Led led;
private Label label;
private HBox container;
private VBox ledContainer;
public ConnectionComponent() {
initGraphics();
}
public Parent getView() {
return this.container;
}
public void initGraphics() {
//Here I set up the elements (label and Led) inside the container
}
Which is called here:
#ServiceProvider(service = StatusLineElementProvider.class)
public class ConnectionIndicator implements StatusLineElementProvider {
#Override
public Component getStatusLineElement() {
JFXPanel fxPanel = new JFXPanel();
Platform.setImplicitExit(false);
new JavaFXUIThread().runOnUiToolkitThread(() -> {
Scene scene = new Scene(new ConnectionComponent().getView());
scene.getStylesheets().add(FXTheme.getDefault().getStylesheet());
fxPanel.setScene(scene);
});
return fxPanel;
}
}
The idea is to showing something on the top (even a simple text message) and, in the meanwhile, make the application in background more opaque.
You need a modal Dialog. Create such a dialog and show it when your connection goes down. Then use a Thread which periodically checks if your connection is back up. The time the connection comes alive kill the dialog. Since the dialog is modal it means that you can do nothing to the UI until it is resolved. See this.
Use Alert or Dialog components. You can style them by CSS or add custom content. Try this simplest solution:
Alert a = new Alert(Alert.AlertType.ERROR, "Connection error");
public void createAlert() {
a.getDialogPane().getButtonTypes().clear();
a.initModality(Modality.APPLICATION_MODAL);
//*************** EDIT ***************
a.initStyle(StageStyle.UNDECORATED);
a.initOwner(label.getScene().getWindow());
//************************************
}
#Override
public void setConnected(boolean connected) {
if (connected) {
Platform.runLater(() -> {
label.setText("Connected");
a.show();
});
} else {
Platform.runLater(() -> {
label.setText("Disconnected");
a.close();
});
}
}
You can also add additional Pane on top of your entire Scene:
StackPane root = new StackPane();
root.getChildren().addAll(applicationContent);
Pane p = new Pane();
p.setStyle("-fx-background-color: rgba(31,31,31,0.6);");
//add Pane to root when disconnected
//root.getChildren().add(p);
Scene scene = new Scene(root, 300, 250);

how to actually hide Tab from TabPane with JavaFX

previously I was working on Java Swing and now I'm trying to work with JavaFX. My Java Swing code last time:
//These line of code is to call method that declared in ContentPage.java
contentPage.adminFeatureEnabled(adminEnabled);
contentPage.managerFeatureEnabled(managerEnabled);
and in my ContentPage.java
//By default, all feature (or tab) are enabled.
//This method is to remove register account if the user login into the system is manager and staff
public void adminFeatureEnabled(boolean a) {
if (!a) {
tabPane.removeTabAt(tabPane.indexOfComponent(registerAccount));
}
}
//This method is to remove register account and purchase order if the user who log into the system is staff
public void managerFeatureEnabled(boolean a) {
if(!a) {
tabPane.removeTabAt(tabPane.indexOfComponent(purchaseOrder));
}
}
and in my code:
if (role.equals("admin")){
contentPage.contentFrame.setTitle("Menu - Admin!");
contentPage.disUser.setEditable(true);
contentPage.chgRoles.setEnabled(true);
} else if(role.equals("manager")){
contentPage.contentFrame.setTitle("Menu - Manager!");
contentPage.chgRoles.setSelectedItem("manager");
adminEnabled = false;
}else if (role.equals("staff")){
contentPage.contentFrame.setTitle("Menu - Staff!");
contentPage.chgRoles.setSelectedItem("staff");
adminEnabled = false;
managerEnabled = false;
}
The code above will perform like this:
when the user login with admin account, all the feature (Tab) enabled
when the user login as manager, some feature (tab) will be hide
My current problem now:
I wanted the same feature as above in JavaFX but I don't know how as none of the method works as I wanted.
anyone can help me with this?
Simply modify the tabs list:
The following example adds/removes Tabs, when the CheckBoxes are (un)selected.
#Override
public void start(Stage primaryStage) {
Tab tab1 = new Tab("Tab 1", new Label("1"));
Tab tab2 = new Tab("Tab 2", new Label("2"));
TabPane tabPane = new TabPane();
tabPane.setPrefSize(400, 400);
CheckBox cb1 = new CheckBox("1");
CheckBox cb2 = new CheckBox("2");
cb1.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
tabPane.getTabs().add(0, tab1);
} else {
tabPane.getTabs().remove(tab1);
}
});
cb2.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
tabPane.getTabs().add(tab2);
} else {
tabPane.getTabs().remove(tab2);
}
});
Scene scene = new Scene(new VBox(new HBox(cb1, cb2), tabPane));
primaryStage.setScene(scene);
primaryStage.show();
}
It's been so long since the question asked, but this maybe helpful for someone.
You can try something like this.
you have a tabPane with three tabs tabOne, tabTwo and tabThree.
position index of tabs
tabOne - 0
tabTwo - 1
tabThree - 2
to hide tabTwo, you can use remove function and again reappear you can use set function.
to remove tab
tabPane.getTabs().remove(tabTwo);
set again with the relevant index to display at the correct location.
tabPane.getTabs().set(1, tabTwo);

JavaFX Textfield - Override default behaviour when up key is released

I am trying to set TextField's cursor/caret to be at the end when the up key is released:
private void setTextFieldBehaviour() {
_textField.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent key) {
switch (key.getCode()) {
case UP:
if (key.isControlDown()) {
... // Do something for Ctrl + UP key
break;
} else {
... // Do something for plain old UP key
_textField.end();
// _textField's cursor will go back
// to the front when the UP key is released!
break;
}
default:
break;
}
}
});
}
However, as stated in the above code's comment, because I am only overriding the method setOnKeyPressed, this means that the default behaviour when the keys are released will kick in for JavaFX's TextField,
Consequently, this makes it difficult to set the TextField cursor at the end when the UP key is released.
I thought of overriding setOnKeyReleased just for UP (as shown below), but this has the ugly effect of the cursor jumping to the front of the TextField, then jumping back at the end.
_textField.setOnKeyReleased(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent key) {
switch(key.getCode()) {
case UP:
if(!(key.isControlDown())) {
... // Do something for plain old UP key
_textField.end();
// _textField's cursor will go back
// to the front when the UP key is released
// then jump back to the end again!
break;
}
default:
break;
}
}
});
Or visually, this is how it appears:
Before pressing anything: // Cursor is placed at the end of the string
TextField[oldString |]
Press UP: // Cursor is in front of string
TextField[| oldString]
Release UP: // Cursor is back of string
TextField[newString |]
So currently, I am able to set it to go to the back, but this entails the cursor jumping around. Is there anyway to do it better?
look at those sample application that show 2 way to achieve this:
by setting the event Handler and consuming the event.
public class main extends Application {
#Override
public void start(Stage primaryStage) {
HBox root = new HBox();
TextField textField = new TextField();
textField.setOnKeyPressed(e -> {
if (e.getCode().equals(KeyCode.UP)) {
textField.end();
e.consume(); // this stop propagating the event
}
});
root.getChildren().add(textField);
Scene scene = new Scene(root, 300, 300);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
or by filtering the event and stop its propagation
public class main extends Application {
#Override
public void start(Stage primaryStage) {
HBox root = new HBox();
TextField textField = new TextField();
textField.addEventFilter(KeyEvent.KEY_PRESSED, e -> { //event filter first catch the event
if(e.getCode().equals(KeyCode.UP)){
textField.end();
e.consume(); // this stop propagating the event
}
});
root.getChildren().add(textField);
Scene scene = new Scene(root, 300, 300);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
You should look at http://docs.oracle.com/javase/8/javafx/events-tutorial/events.htm to learn how event are processed in javaFX.

How can I change the GUI of JavaFX outside start()?

I am crazy about the feature of JavaFX, in Swing, I could do,
#Override
public void onPluginRegistered(final GamePlugin plugin) {
JRadioButtonMenuItem gameMenuItem = new JRadioButtonMenuItem(plugin.getGameName());
gameMenuItem.setSelected(false);
gameMenuItem.addActionListener(event -> {
if (core.getPlayers().isEmpty()) {
// Can't start a game with no players.
showErrorDialog(frame, ERROR_NO_PLAYERS_TITLE, ERROR_NO_PLAYERS_MSG);
gameGroup.clearSelection();
} else {
core.startNewGame(plugin);
}
});
gameGroup.add(gameMenuItem);
newGameMenu.add(gameMenuItem);
}
if I want to add a radio item whenever a plugin has registered.
However in JavaFX, it seems, you can't declare any global item of JavaFX, because once the start() is called, it starts a new constructor and everything you've done before is nothing (there is no variable share to me).
Here is my Javafx code.
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane root = new BorderPane();
Scene scene = new Scene(root, 500, 500);
scene.getStylesheets().add("./Buttons.css");
Region spacer = new Region();
spacer.setMinWidth(10);
primaryStage.setScene(scene);
primaryStage.show();
TabPane tabPane = new TabPane();
Tab tabData = new Tab("Get your data");
tabPane.getTabs().add(tabData);
Tab tabDisplay = new Tab("Visualize your data");
tabPane.getTabs().add(tabDisplay);
pluginGroup.selectedToggleProperty().addListener(new ChangeListener<Toggle>(){
#Override
public void changed(ObservableValue<? extends Toggle> ov,
Toggle old_toggle, Toggle new_toggle) {
if (pluginGroup.getSelectedToggle() != null) {
RadioButton chk = (RadioButton) new_toggle.getToggleGroup().getSelectedToggle();
chk.getText();
}
}
});
root.setCenter(tabPane);
FlowPane inputPanel = new FlowPane();
TextField source = new TextField ();
Button confirmButton = new Button("Get Your Resource!");
confirmButton.getStyleClass().add("GREEN");
inputPanel.getChildren().addAll(new Label("Input your source:"),
spacer, source, confirmButton);
root.setBottom(inputPanel);
RadioButton defaultBtn = new RadioButton("No data plugin are registered");
FlowPane pane = new FlowPane();
pane.getChildren().addAll(new Label("Select your data source"), spacer);
if (radioButtonBox != null) {
pane.getChildren().add(radioButtonBox);
}
tabData.setContent(pane);
}
#Override
public void onPluginRegistered(DataPlugin plugin) {
RadioButton button = new RadioButton(plugin.getName());
button.setToggleGroup(pluginGroup);
radioButtonBox.getChildren().add(button);
}
public void caller(String[] args) {
launch(args);
}
I want to initialize the javafx program from,
public static void main(String[] args) throws Exception {
DataFramework core = new ConcreteDataFramework();
GuiFramework gui = new GuiFramework(core);
core.addGuiListener(gui);
gui.caller(args);
core.registerPlugin(new CsvData());
}
It is weird that I can't add any radio button to the existing radioButtonBox every time I call onPluginRegistered(DataPlugin plugin) (The new radiobutton does not show up)
You should consider the start() method as the replacement for the main method. If your application needs access to some kind of service or model, create it in the start() (or init()) method. I would actually recommend making the Application subclass (which is inherently not reusable) as minimal as possible - it should just do the startup work - and factoring the remaining GUI code into a separate class. (If you use FXML, the FXML file can define the UI, and the Application subclass is then already pretty minimal: it just loads and displays the FXML.)
You haven't really provided enough context to make it clear what's going on here, but I'm guessing GuiFramework is the Application subclass you've shown part of, and DataFramework is an interface of some kind. I also assume GuiFramework is implementing some interface that defines the onPluginRegistered method.
So I would do:
public class GuiFramework implements PluginAware {
private final BorderPane root ;
private final DataFramework dataFramework ;
public GuiFramework(DataFramework dataFramework) {
this.dataframework = dataFramework ;
this.root = new BorderPane();
TabPane tabPane = new TabPane();
Tab tabData = new Tab("Get your data");
tabPane.getTabs().add(tabData);
// etc etc (remaining code from your start() method)
}
public Parent getView() {
return root ;
}
#Override
public void onPluginRegistered(DataPlugin plugin) {
RadioButton button = new RadioButton(plugin.getName());
button.setToggleGroup(pluginGroup);
radioButtonBox.getChildren().add(button);
}
}
and define a Main class for starting the application:
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
DataFramework core = new ConcreteDataFramework();
GuiFramework gui = new GuiFramework(core);
core.addGuiListener(gui);
Scene scene = new Scene(gui.getView(), 500, 500);
scene.getStylesheets().add("./Buttons.css");
primaryStage.setScene(scene);
primaryStage.show();
core.registerPlugin(new CsvData());
}
// for environments not supporting JavaFX launch automatically:
public static void main(String[] args) {
launch(args);
}
}

Categories

Resources