JavaFX "quickfixes" with tooltips and hyperlinks - user-interface

does JavaFX provide something like Eclipse Quickfixes? Meaning that you hover over a thing that is broken and got some solutions for it that you can apply immediately.
I know that there are tooltips but they can only contain text, I would need something clickable. Another solution would be something like Dialogs, but I don't want to open another window. I want it to appear on the current stage.
Any suggestions?
Edit: to make it clear, I want to adopt the concept of eclipse quickfixes onto a JavaFX based application, maybe showing a "quickfix" when hovering over a circle instance. I don't want to check any (java/javafx) source code.
Edit2: I've got a hyperlink on a tooltip now:
HBox box = new HBox();
Tooltip tooltip = new Tooltip();
tooltip.setText("Select an option:");
tooltip.setGraphic(new Hyperlink("Option 1"));
Tooltip.install(box, tooltip);
I've got three new problems now:
How to make the tooltip not disappear when leaving the HBox and staying there when entering the mouse into the tooltip?
How to add mulitple graphics / hyperlinks? Is it even possible?
How to first show the text and then, in a new line, display the graphics?
Thanks in advance!

You can add any node to a tooltip using the setGraphic() method. Here is a simple example demonstrating using a tooltip for "quick fix" functionality:
import java.util.Random;
import javafx.application.Application;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TooltipWithQuickfix extends Application {
#Override
public void start(Stage primaryStage) {
TextField textField = new TextField();
textField.pseudoClassStateChanged(PseudoClass.getPseudoClass("invalid"), true);
textField.setTextFormatter(new TextFormatter<Integer>(c -> {
if (c.getText().matches("\\d*")) {
return c ;
}
return null ;
}));
textField.textProperty().isEmpty().addListener((obs, wasEmpty, isNowEmpty) ->
textField.pseudoClassStateChanged(PseudoClass.getPseudoClass("invalid"), isNowEmpty));
Tooltip quickFix = new Tooltip();
Hyperlink setToDefault = new Hyperlink("Set to default");
Hyperlink setToRandom = new Hyperlink("Set to random");
setToDefault.setOnAction(e -> {
textField.setText("42");
quickFix.hide();
});
Random rng = new Random();
setToRandom.setOnAction(e -> {
textField.setText(Integer.toString(rng.nextInt(100)));
quickFix.hide();
});
VBox quickFixContent = new VBox(new Label("Field cannot be empty"), setToDefault, setToRandom);
quickFixContent.setOnMouseExited(e -> quickFix.hide());
quickFix.setGraphic(quickFixContent);
textField.setOnMouseEntered(e -> {
if (textField.getText().isEmpty()) {
quickFix.show(textField, e.getScreenX(), e.getScreenY());
}
});
VBox root = new VBox(textField);
root.getStylesheets().add("style.css");
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
with the stylesheet (style.css):
.root {
-fx-alignment: center ;
-fx-padding: 24 10 ;
}
.text-field:invalid {
-fx-control-inner-background: #ff7979 ;
-fx-focus-color: red ;
}

Related

How to animate several nodes with pause between each one?

I am trying to animate a series of nodes one after the other in a loop. The goal is to have the first node begin its animation, followed by a short pause before the next node begins to animate.
However, when running this within a loop, it executes too fast and all nodes appear to be animating at the same time.
For simplicity, I am using the AnimateFX library to handle the animations, but I assume the functionality needed here would apply in other situations?
How would I add a pause between each of the HBox animations?
import animatefx.animation.Bounce;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class AnimationTest extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
final VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
final HBox tiles = new HBox(5);
tiles.setAlignment(Pos.CENTER);
// Create 5 tiles
for (int i = 0; i < 5; i++) {
HBox tile = new HBox();
tile.setPrefSize(50, 50);
tile.setStyle("-fx-border-color: black; -fx-background-color: lightblue");
tiles.getChildren().add(tile);
}
Button button = new Button("Animate");
button.setOnAction(event -> {
// Animate each tile, one at a time
for (Node child : tiles.getChildren()) {
Bounce animation = new Bounce(child);
animation.play();
}
});
root.getChildren().add(tiles);
root.getChildren().add(button);
primaryStage.setWidth(500);
primaryStage.setHeight(200);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
}
I don't know AnimateFX, but using the standard libraries you can add animations to a SequentialTransition.
For example, to animate each node but starting at a later time, add PauseTransitions of increasing duration and the desired animation to SequentialTransitions, and play the SequentialTransitions.
As I said, I'm not familiar with the library you're using, but I think it would look like this:
Button button = new Button("Animate");
button.setOnAction(event -> {
Duration offset = Duration.millis(500);
Duration start = new Duration();
// Animate each tile, one at a time
for (Node child : tiles.getChildren()) {
Bounce bounce = new Bounce(child);
PauseTransition delay = new PauseTransition(start);
SequentialTransition animation = new SequentialTransition(delay, bounce.getTimeline());
animation.play();
start = start.add(offset);
}
});

Javafx size of a Button after adding it to a Container

In case a button is added dynamically into a layout, the getWidth property adds back 0; However, the preferred size is reachable instantly. I'm assuming it's because the system didn't have a chance to calculate the size of the button ( since it's just added ).
Minimum reproducible example:
import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class AgentApp extends Application {
public static void main(String[] args){
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
Stage tagTest = new Stage();
VBox topBox = new VBox();
Parent tagRoot = topBox;
Button btn = new Button("Add a Button dynamically");
btn.setOnAction(event -> {
Button dBtn = new Button("How big is this?!");
dBtn.setOnAction(event1 -> System.out.println("Width is actually: " + dBtn.getWidth())); /* (1) */
topBox.getChildren().add(dBtn);
System.out.println("Width:" + dBtn.getWidth()); /* (2) */
});
topBox.getChildren().add(btn);
Scene tagsScene = new Scene(tagRoot,400,200);
tagTest.setScene(tagsScene);
tagTest.show();
}
}
at (1) the width of the button is printed out correctly, while the width at (2) prints out 0.0. Which is unexpected.
Calling the layout function on the parent of the node ( topbox ) yields no results; Neither inheriting and calling the protected function layoutChildren in a custom container.
But somewhere down the line I assume the size itself must be calculated somewhere, since the size is calculated by the time a dBtn is pressed. How can the size calculation be forced at that point?
UPDATE:
Asking for the width asynchronously returns the correct size:
Button dBtn = new Button("How big is this?!");
dBtn.setOnAction(event1 -> System.out.println("Width is actually: " + dBtn.getWidth()));
topBox.getChildren().add(dBtn);
Platform.runLater(() -> System.out.println("btn width: " + btn.getWidth()));
But since that's an asynchronous call to be run in an unspecified time, the size is still not available instantly.
you have to add the Node into the Scene before calling the getWif

Is it possible to consume the event from a JavaFX FileChooser window?

I have a JavaFX Button that triggers when the user presses enter. This causes a FileChooser to open up. Some people (like myself) may hit enter inside the FileChooser to save the file. However, this causes the save button to trigger itself again and open the FileChooser again to save a new file. Clicking the button (in the FileChooser) with the mouse does not have this issue.
I thought consuming the event from the button would do something about this issue, but it only consumes the button on the GUI's event, rather than the FileChooser button. I've tried looking for ways to modify the FileChooser's EventHandler to consume an enter keypress, but with no success.
I've also tried taking the focus off the button and moving it to the parent (a Pane) so it can't be clicked again. However, there are buttons that benefit being clicked multiple times without having to regain focus on them again.
A example of my code looks like this (obviously this would be part of a bigger class that extends Application):
EventHandler<KeyEvent> enter = event -> {
if (event.getCode() == KeyCode.ENTER && event.getSource() instanceof Button) {
Button src = (Button) event.getSource();
src.fire();
}
event.consume();
};
Button b1 = new Button("Save");
b1.setOnKeyReleased(enter);
/* Called by .fire method */
b1.setOnAction(event -> {
/* Create the save dialog box */
FileChooser saveDialog = new FileChooser();
saveDialog.setTitle("Save");
/* Get file */
File f = saveDialog.showSaveDialog(stage);
/*
* ... do stuff with file ...
*/
});
Note: This example isn't my exact code. Instead the key released event is a variable used for multiple buttons, rather than just the save button (i.e. b2.setOnKeyReleased(enter);
b2.setOnAction(event -> {/* Do something */});).
How could I go about preventing the button from triggering when the user presses enter in the FileChooser? I don't want the user to be stuck in a loop if they don't have a mouse. I'm aware that pressing Alt+S also saves it, but I can't expect all users to be aware of that.
EDIT: As requested in a comment which appears to be deleted now, here's a runnable version of the code:
import java.io.File;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
public class ButtonTest extends Application {
#Override
public void start(Stage stage) throws Exception {
/* EventHandler to be used with multiple buttons */
EventHandler<KeyEvent> enter = event -> {
if (event.getCode() == KeyCode.ENTER && event.getSource() instanceof Button) {
Button src = (Button) event.getSource();
src.fire();
}
event.consume();
};
/* Create a new button */
Button b1 = new Button("Save");
Button b2 = new Button("Print");
/* Add event handlers */
b1.setOnKeyReleased(enter);
b2.setOnKeyReleased(enter);
/* Called by .fire method of save button */
b1.setOnAction(event -> {
/* Create the save dialog box */
FileChooser saveDialog = new FileChooser();
saveDialog.setTitle("Save");
/* Get file */
File f = saveDialog.showSaveDialog(stage);
/* ... do stuff with file ... */
});
/* Called by .fire method of print button */
b2.setOnAction(event -> System.out.println("Pressed"));
Scene scene = new Scene(new HBox(b1, b2));
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
The problem is firing the Button from the onKeyReleased handler. By the time you release the ENTER key the FileChooser has been hidden and the Stage has regained focus, meaning the key-release event is given to your Stage/Button. Obviously this will cause a cycle.
One possible solution is to fire the Button from inside a onKeyPressed handler. This will give slightly different behavior relative to other applications, however, which your users might not expect/appreciate.
Another possible solution is to track if the FileChooser had been open before firing the Button, like what Matt does in his answer.
What you seem to be trying to do is allow users to use the ENTER key to fire the Button; this should be default behavior on platforms like Windows.
Not for me. Space is the only key that triggers a button. I think the reason for that is because enter is used to trigger the default button which is set using btn.setDefaultButton(true);
For me, pressing ENTER while the Button has focus fires the action event when using JavaFX 11.0.2 but not JavaFX 8u202, both on Windows 10. It appears the behavior of Button changed since JavaFX 8. Below is the different implementations of com.sun.javafx.scene.control.behavior.ButtonBehavior showing the registered key bindings.
JavaFX 8u202
protected static final List<KeyBinding> BUTTON_BINDINGS = new ArrayList<KeyBinding>();
static {
BUTTON_BINDINGS.add(new KeyBinding(SPACE, KEY_PRESSED, PRESS_ACTION));
BUTTON_BINDINGS.add(new KeyBinding(SPACE, KEY_RELEASED, RELEASE_ACTION));
}
JavaFX 11.0.2
public ButtonBehavior(C control) {
super(control);
/* SOME CODE OMITTED FOR BREVITY */
// then button-specific mappings for key and mouse input
addDefaultMapping(buttonInputMap,
new KeyMapping(SPACE, KeyEvent.KEY_PRESSED, this::keyPressed),
new KeyMapping(SPACE, KeyEvent.KEY_RELEASED, this::keyReleased),
new MouseMapping(MouseEvent.MOUSE_PRESSED, this::mousePressed),
new MouseMapping(MouseEvent.MOUSE_RELEASED, this::mouseReleased),
new MouseMapping(MouseEvent.MOUSE_ENTERED, this::mouseEntered),
new MouseMapping(MouseEvent.MOUSE_EXITED, this::mouseExited),
// on non-Mac OS platforms, we support pressing the ENTER key to activate the button
new KeyMapping(new KeyBinding(ENTER, KeyEvent.KEY_PRESSED), this::keyPressed, event -> PlatformUtil.isMac()),
new KeyMapping(new KeyBinding(ENTER, KeyEvent.KEY_RELEASED), this::keyReleased, event -> PlatformUtil.isMac())
);
/* SOME CODE OMITTED FOR BREVITY */
}
As you can see, both register SPACE to fire the Button when it has focus. However, the JavaFX 11.0.2 implementation also registers ENTER for the sameā€”but only for non-Mac OS platforms. I couldn't find any documentation about this change in behavior.
If you want the same behavior in JavaFX 8, and you don't mind hacking into the internals of JavaFX, then you can use reflection to alter the behavior of all button-like controls in your application. Here's a utility method example:
import com.sun.javafx.PlatformUtil;
import com.sun.javafx.scene.control.behavior.ButtonBehavior;
import com.sun.javafx.scene.control.behavior.KeyBinding;
import java.lang.reflect.Field;
import java.util.List;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public final class ButtonUtils {
public static void installEnterFiresButtonFix() throws ReflectiveOperationException {
if (PlatformUtil.isMac()) {
return;
}
Field bindingsField = ButtonBehavior.class.getDeclaredField("BUTTON_BINDINGS");
Field pressedActionField = ButtonBehavior.class.getDeclaredField("PRESS_ACTION");
Field releasedActionField = ButtonBehavior.class.getDeclaredField("RELEASE_ACTION");
bindingsField.setAccessible(true);
pressedActionField.setAccessible(true);
releasedActionField.setAccessible(true);
#SuppressWarnings("unchecked")
List<KeyBinding> bindings = (List<KeyBinding>) bindingsField.get(null);
String pressedAction = (String) pressedActionField.get(null);
String releasedAction = (String) releasedActionField.get(null);
bindings.add(new KeyBinding(KeyCode.ENTER, KeyEvent.KEY_PRESSED, pressedAction));
bindings.add(new KeyBinding(KeyCode.ENTER, KeyEvent.KEY_RELEASED, releasedAction));
}
private ButtonUtils() {}
}
You would call this utility method early in the startup of your application, before any Buttons are created. Here's an example using it:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
ButtonUtils.installEnterFiresButtonFix();
} catch (ReflectiveOperationException ex) {
ex.printStackTrace();
}
Button button = new Button("Save");
button.setOnAction(event -> {
event.consume();
System.out.println(new FileChooser().showSaveDialog(primaryStage));
});
Scene scene = new Scene(new StackPane(button), 300, 150);
primaryStage.setScene(scene);
primaryStage.setTitle("Workshop");
primaryStage.show();
}
}
Reminder: This fix is implementation dependent.
I added a boolean for the fileChooser being open and it seems to be working for me but I had to split the events up otherwise it will only fire the print button every other
public class Main extends Application {
private boolean fileChooserOpen = false;
#Override
public void start(Stage stage) throws Exception{
/* EventHandler to be used with multiple buttons */
EventHandler<KeyEvent> enterWithFileChooser = event -> {
if (!fileChooserOpen && event.getCode() == KeyCode.ENTER && event.getSource() instanceof Button) {
Button src = (Button) event.getSource();
src.fire();
fileChooserOpen = true;
}else {
fileChooserOpen = false;
}
event.consume();
};
EventHandler<KeyEvent> enter = event -> {
if (event.getCode() == KeyCode.ENTER && event.getSource() instanceof Button) {
Button src = (Button) event.getSource();
src.fire();
}
event.consume();
};
/* Create a new button */
Button b1 = new Button("Save");
Button b2 = new Button("Print");
/* Add event handlers */
b1.setOnKeyReleased(enterWithFileChooser);
b2.setOnKeyReleased(enter);
/* Called by .fire method of save button */
b1.setOnAction(event -> {
/* Create the save dialog box */
FileChooser saveDialog = new FileChooser();
saveDialog.setTitle("Save");
/* Get file */
File f = saveDialog.showSaveDialog(stage);
/* ... do stuff with file ... */
});
/* Called by .fire method of print button */
b2.setOnAction(event -> System.out.println("Pressed"));
Scene scene = new Scene(new HBox(b1, b2));
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) { launch(args); }
}

What aspect of a a JavaFX animation is missing from this?

I am trying to get this simple animation to play over an image background however I cannot get it to start.
I have tried adding in a button as well as using playFromStart() instead of play().
I also tried adding in the set orientation to the path, I didn't think it would do anything because I'm just moving a circle, and it hasn't helped.
I also tried messing with the timing and number of repetitions of the animation just in case somehow it was just all happening really quickly or slowly and I just missed it.
I feel like I'm probably missing something really simple but from everything that I've looked at, all the things that the examples have, I also have.
The background image also went away when I added the button, for that I have tried moving it up and other things but I feel like this is also a simple issue that my brain has just glazed over.
package javafxapplication10;
import javafx.animation.PathTransition;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.image.*;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.ImagePattern;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import javafx.util.Duration;
public class JavaFXApplication10 extends Application {
#Override
public void start(Stage stage) {
stage.setScene(scene);
stage.setResizable(false);
stage.sizeToScene();
ImagePattern pattern = new ImagePattern(image);
scene.setFill(pattern);
stage.setScene(scene);
stage.show();
Circle cir = new Circle (19);
cir.setLayoutX(170);
cir.setLayoutY(100);
cir.setFill(Color.KHAKI);
pane.getChildren().add(cir);
Path path1 = new Path();
path1.getElements().add(new MoveTo(170,650));
path1.getElements().add(new MoveTo(1335,650));
path1.getElements().add(new MoveTo(1335,100));
PathTransition pl = new PathTransition();
pl.setDuration(Duration.seconds(8));
pl.setPath(path1);
pl.setNode(cir);
pl.setCycleCount(1);
//pl.setOrientation(OrientationType.ORTHOGONAL_TO_TANGENT);
pl.setAutoReverse(false);
//pl.play();
Button begin = new Button("Begin");
begin.setLayoutX(780);
begin.setLayoutY(105);
begin.setOnAction(new EventHandler<ActionEvent> () {
#Override
public void handle(ActionEvent press) {
pl.play();
}
});
pane.getChildren().add(begin);
}
Image image = new Image("file:Figure one.png");
Pane pane = new Pane();
Scene scene = new Scene (pane,1474,707);
public static void main(String[] args) {
launch(args);
}
}
PathTransition only moves the node along a path that would actually be drawn. MoveTo elements do not draw anything, but simply set the current position. You need to use LineTo (and/or ClosePath) to draw something in the Path. Furthermore PathTransition sets the translate poperties, not the layout properties, i.e. final position of the circle is determined by adding the layout coordinates to the coodrinates provided by the Path. Therefore you should either position the Circle using the translate properties or start the path at (0, 0):
Path path1 = new Path(
new MoveTo(0, 0),
new LineTo(0, 550),
new LineTo(1165, 550),
new LineTo(1165, 0),
new ClosePath()
);
// path1.getElements().add(new MoveTo(170,650));
// path1.getElements().add(new MoveTo(1335,650));
// path1.getElements().add(new MoveTo(1335,100));

JavaFX draggable background - alternative to using a camera?

I am trying to implement a navigational feature for an editor I am working on, that would allow you to move around in the window by dragging on the background - basically like you can move around in Open Maps.
My current approach is to move a scene-camera around via DragEvent-Listeners on the scene, in which the displayed objects are children to the root Group.
However, I am wondering whether there is another way to implement this that would not require the use of a camera.
The following works as a simple test:
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class InfinitePanning extends Application {
#Override
public void start(Stage primaryStage) {
Pane drawingPane = new Pane();
drawingPane.setStyle("-fx-background-color: white;");
Scene scene = new Scene(drawingPane, 800, 800, Color.WHITE);
scene.setOnScroll(e -> {
drawingPane.setTranslateX(drawingPane.getTranslateX() + e.getDeltaX());
drawingPane.setTranslateY(drawingPane.getTranslateY() + e.getDeltaY());
});
scene.setOnMouseClicked(e -> {
if (e.getClickCount() == 2) {
Point2D center = drawingPane.sceneToLocal(new Point2D(e.getX(), e.getY()));
Circle c = new Circle(center.getX(), center.getY(), 25, Color.CORNFLOWERBLUE);
drawingPane.getChildren().add(c);
}
});
Circle c = new Circle(50, 50, 25, Color.CORNFLOWERBLUE);
drawingPane.getChildren().add(c);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Double-click to add a new circle, scroll with the mouse (scroll button/wheel or scroll gesture on a trackpad) to move around.
There are some subtleties here. The pane is initially sized to fit the scene; as you scroll around the mouse will be outside the bounds of the pane. If you double-click outside the bounds of the pane (so you add a new node whose parameters are outside the bounds), then the pane expands at that point to include the new child.

Resources