Delegate mouse events to all children in a JavaFX StackPane - javafx

I'm trying to come up with a solution to allow multiple Pane nodes handle mouse events independently when assembled into a StackPane
StackPane
Pane 1
Pane 2
Pane 3
I'd like to be able to handle mouse events in each child, and the first child calling consume() stops the event going to the next child.
I'm also aware of setPickOnBounds(false), but this does not solve all cases as some of the overlays will be pixel based with Canvas, i.e. not involving the scene graph.
I've tried various experiments with Node.fireEvent(). However these always lead to recursion ending in stack overflow. This is because the event is propagated from the root scene and triggers the same handler again.
What I'm looking for is some method to trigger the event handlers on the child panes individually without the event travelling through its normal path.
My best workaround so far is to capture the event with a filter and manually invoke the handler. I'd need to repeat this for MouseMoved etc
parent.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
for (Node each : parent.getChildren()) {
if (!event.isConsumed()) {
each.getOnMouseClicked().handle(event);
}
}
event.consume();
});
However this only triggers listeners added with setOnMouseClicked, not addEventHandler, and only on that node, not child nodes.
Another sort of solution is just to accept JavaFX doesn't work like this, and restructure the panes like this, this will allow normal event propagation to take place.
Pane 1
Pane 2
Pane 3
Example
import javafx.application.Application;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class EventsInStackPane extends Application {
public static void main(String[] args) {
launch(args);
}
private static class DebugPane extends Pane {
public DebugPane(Color color, String name) {
setBackground(new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY)));
setOnMouseClicked(event -> {
System.out.println("setOnMouseClicked " + name + " " + event);
});
addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
System.out.println("addEventHandler " + name + " " + event);
});
addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
System.out.println("addEventFilter " + name + " " + event);
});
}
}
#Override
public void start(Stage primaryStage) throws Exception {
DebugPane red = new DebugPane(Color.RED, "red");
DebugPane green = new DebugPane(Color.GREEN, "green");
DebugPane blue = new DebugPane(Color.BLUE, "blue");
setBounds(red, 0, 0, 400, 400);
setBounds(green, 25, 25, 350, 350);
setBounds(blue, 50, 50, 300, 300);
StackPane parent = new StackPane(red, green, blue);
eventHandling(parent);
primaryStage.setScene(new Scene(parent));
primaryStage.show();
}
private void eventHandling(StackPane parent) {
parent.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (!event.isConsumed()) {
for (Node each : parent.getChildren()) {
Event copy = event.copyFor(event.getSource(), each);
parent.fireEvent(copy);
if (copy.isConsumed()) {
break;
}
}
}
event.consume();
});
}
private void setBounds(DebugPane panel, int x, int y, int width, int height) {
panel.setLayoutX(x);
panel.setLayoutY(y);
panel.setPrefWidth(width);
panel.setPrefHeight(height);
}
}

Using the hint from #jewelsea I was able to use a custom chain. I've done this from a "catcher" Pane which is added to the front of the StackPane. This then builds a chain using all the children, in reverse order, excluding itself.
private void eventHandling(StackPane parent) {
Pane catcher = new Pane() {
#Override
public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
EventDispatchChain chain = super.buildEventDispatchChain(tail);
for (int i = parent.getChildren().size() - 1; i >= 0; i--) {
Node child = parent.getChildren().get(i);
if (child != this) {
chain = chain.prepend(child.getEventDispatcher());
}
}
return chain;
}
};
parent.getChildren().add(catcher);
}

Related

How can I change the scene By pressing a specific key(b) on the the keyboard?

In my application, there are two scenes: mainScene and bossScene where mainScene is used when starting up the application.
I'm trying to implement the boss key functionality where by pressing the 'b' key on the the keyboard should change the scene to bossScene. And also by pressing the button in bossScene should switch back to mainScene.
I'm getting an error on InteliJ saying "Cannot resolve method setOnKeyPressed in List
My Code:
public void start(Stage stage) throws Exception {
stage.setTitle("BossKey Example");
// Scene and layout for the main view
VBox root = new VBox();
Scene mainScene = new Scene(root, 500, 300);
// Scene for the BOSS view
Scene bossScene = new Scene(new Label("Nothing suspicious here"), 500, 300);
List<TextField> fields = new ArrayList<TextField>();
for (int i = 0; i < 10; i++) {
fields.add(new TextField());
}
fields.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent keyEvent) {
switch (keyEvent.getCharacter()){
case "b": stage.setScene(bossScene); break;
}
}
});
/////// Added addEventFilter, still not working
mainScene.addEventFilter(KeyEvent.KEY_PRESSED, new
EventHandler<KeyEvent() {
#Override
public void handle(KeyEvent keyEvent) {
switch (keyEvent.getCharacter()){
case "b": stage.setScene(bossScene); break;
}
keyEvent.consume();
}
});
// Create components for main view
root.getChildren().addAll(fields);
root.getChildren().add(new Button("Hello!"));
stage.setScene(mainScene);
stage.show();
}
}
KeyCombination filters
You should use a key combination in an event filter, e.g., CTRL+B or SHORTCUT+B.
For details on how to apply key combinations, see:
javafx keyboard event shortcut key
Why a key combination is superior to filtering on the character "b":
If you filter on a "b" character, the feature won't work if caps lock is down.
If you filter on a "b" character, you will be unable to type "b" in the text field.
You might think you could write scene.setOnKeyPressed(...), however, that won't work as expected in many cases. A filter is required rather than a key press event handler because the key events may be consumed by focused fields like text fields if you use a handler, so a handler implementation might not activate in all desired cases.
Filtering on a key combination avoids the issues with trying to handle a character key press. The key combinations rely on key codes which represent the physical key pressed and don't rely on the state of other keys such as caps lock unless you explicitly add additional logic for that.
If you don't understand the difference between an event filter and an event handler and the capturing and bubbling phases of event dispatch, then study:
the oracle event handling tutorial.
KeyCombination filter implementation
final EventHandler<KeyEvent> bossEventFilter = new EventHandler<>() {
final KeyCombination bossKeyCombo = new KeyCodeCombination(
KeyCode.B,
KeyCombination.CONTROL_DOWN
);
public void handle(KeyEvent e) {
if (bossKeyCombo.match(e)) {
if (stage.getScene() == mainScene) {
stage.setScene(bossScene);
} else if (stage.getScene() == bossScene) {
stage.setScene(mainScene);
}
e.consume();
}
}
};
mainScene.addEventFilter(KeyEvent.KEY_PRESSED, bossEventFilter);
bossScene.addEventFilter(KeyEvent.KEY_PRESSED, bossEventFilter);
Accelerator alternative
An accelerator could be used instead of an event filter. Information on applying an accelerator is also in an answer to the linked question, I won't detail this alternative further here.
Example Solution
Standalone executable example code:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.*;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.io.IOException;
public class SceneSwap extends Application {
#Override
public void start(Stage stage) throws IOException {
final Scene mainScene = new Scene(
createLayout(
"Press CTRL+B to enter boss mode",
Color.PALEGREEN
)
);
final Scene bossScene = new Scene(
createLayout(
"Press CTRL+B to exit boss mode",
Color.PALEGOLDENROD
)
);
final EventHandler<KeyEvent> bossEventFilter = new EventHandler<>() {
final KeyCombination bossKeyCombo = new KeyCodeCombination(
KeyCode.B,
KeyCombination.CONTROL_DOWN
);
public void handle(KeyEvent e) {
if (bossKeyCombo.match(e)) {
if (stage.getScene() == mainScene) {
stage.setScene(bossScene);
} else if (stage.getScene() == bossScene) {
stage.setScene(mainScene);
}
e.consume();
}
}
};
mainScene.addEventFilter(KeyEvent.KEY_PRESSED, bossEventFilter);
bossScene.addEventFilter(KeyEvent.KEY_PRESSED, bossEventFilter);
stage.setScene(mainScene);
stage.show();
}
private VBox createLayout(String text, Color color) {
VBox mainLayout = new VBox(10,
new Label(text),
new TextField()
);
mainLayout.setPadding(new Insets(10));
mainLayout.setStyle("-fx-background: " + toCssColor(color));
return mainLayout;
}
private String toCssColor(Color color) {
int r = (int) Math.round(color.getRed() * 255.0);
int g = (int) Math.round(color.getGreen() * 255.0);
int b = (int) Math.round(color.getBlue() * 255.0);
int o = (int) Math.round(color.getOpacity() * 255.0);
return String.format("#%02x%02x%02x%02x" , r, g, b, o);
}
public static void main(String[] args) {
launch();
}
}

setOn"Fast"Click - JavaFx

I have a Button that I can move it from the screen, when clicking it has an action. The problem is, when I do Drag'n Drop the click event is called when I release the mouse on, I tried it:
setOnMouseClicked
setOnAction
setOnMousePressed
How can I do to just call the click function when it is a quick click, something like Android times that can differentiate because we have setOnLongClick, so differentiated when I have doing Drag'n Drop and when I really want to click?
Ex:
To move, do:
button.setOnMouseDragged(e -> {
//code move
});
To eventClick:
button.setOnMouseClicked/ Action / MousePressed (e -> {
//call method
});
But when I drop it, it calls setOnMouseClicked / Action / MousePressed, what I want is for it to just call in case I give a quick click, when I drop the drag'n drop do not call.
One option is to keep track of whether or not the Button was dragged; if not, only then execute the code in the onAction handler. Here's an example:
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
public class Main extends Application {
private Point2D origin;
private boolean wasDragged;
#Override
public void start(Stage primaryStage) {
Button button = new Button("Drag me!");
button.setOnAction(this::onAction);
button.setOnMousePressed(this::onMousePressed);
button.setOnMouseDragged(this::onMouseDragged);
button.setOnMouseReleased(this::onMouseReleased);
primaryStage.setScene(new Scene(new Group(button), 800, 600));
primaryStage.show();
}
private void onAction(ActionEvent event) {
event.consume();
if (!wasDragged) {
System.out.println("onAction");
}
}
private void onMousePressed(MouseEvent event) {
event.consume();
origin = new Point2D(event.getX(), event.getY());
System.out.println("onMousePressed");
}
private void onMouseDragged(MouseEvent event) {
event.consume();
wasDragged = true;
Button source = (Button) event.getSource();
source.setTranslateX(source.getTranslateX() + event.getX() - origin.getX());
source.setTranslateY(source.getTranslateY() + event.getY() - origin.getY());
}
private void onMouseReleased(MouseEvent event) {
event.consume();
origin = null;
wasDragged = false;
System.out.println("onMouseReleased");
System.out.println();
}
}
Unfortunately, I can't find documentation guaranteeing the onAction handler is always called before the onMouseReleased handler, but this worked on both Java 8u202 and JavaFX 11.0.2 when I tried it.

SplitPane dividers synchronization JavaFX

I want to synchronize dividers in SplitPane, when divider(0) moves, I also want to make the same move by divider(1). I guess I have to bind the positionProperty of divider(0) with something.
How can I achieve this?
You need to add listeners to the positions of each divider, and update the "linked" divider when it changes. It's important to make sure you don't end up in an infinite recursive loop; the simplest way to do this is to set a flag indicating your updating, and not propagate the update if it's set.
Here's a proof-of-concept example that binds two dividers so the portion between them is always 1/3 of the split pane:
import java.util.List;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.SplitPane;
import javafx.scene.control.SplitPane.Divider;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
public class SplitPaneDemo extends Application {
// helper class that binds two divider positions so the portion between them
// is always 1/3 of the split pane
private static class DividerPositionBinder {
private static final double ONE_THIRD = 1.0/3.0;
private boolean updating ;
DividerPositionBinder(List<Divider> dividers) {
dividers.get(0).positionProperty().addListener((obs, oldPos, newPos) -> {
// don't propagate update if already in an update:
if (updating) return ;
// special handling for right edge of split pane:
if (newPos.doubleValue() > 1.0 - ONE_THIRD) {
dividers.get(0).setPosition(1.0 - ONE_THIRD);
dividers.get(1).setPosition(1.0);
return ;
}
// make right divider the new value + 1/3:
updating = true ;
dividers.get(1).setPosition(newPos.doubleValue() + ONE_THIRD);
updating = false ;
});
dividers.get(1).positionProperty().addListener((obs, oldPos, newPos) -> {
// don't propagate update if already in an update:
if (updating) return ;
// special handling for left edge of split pane:
if (newPos.doubleValue() < ONE_THIRD) {
dividers.get(1).setPosition(ONE_THIRD);
dividers.get(0).setPosition(0.0);
return ;
}
// make left divider the new value - 1/3:
updating = true ;
dividers.get(0).setPosition(newPos.doubleValue() - ONE_THIRD);
updating = false ;
});
}
}
#Override
public void start(Stage primaryStage) {
Region left = new Pane();
left.setStyle("-fx-background-color: coral; ");
Region middle = new Pane();
middle.setStyle("-fx-background-color: aquamarine ;");
Region right = new Pane();
right.setStyle("-fx-background-color: cornflowerblue ;");
SplitPane splitPane = new SplitPane(left, middle, right);
new DividerPositionBinder(splitPane.getDividers());
Scene scene = new Scene(splitPane, 800, 800);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

How to pass ScrollPane event KEY_PRESSED, KeyCode.LEFT to parent pane?

I have a StackPane with a single child - ScrollPane. I am trying to handle KEY_PRESSED, KeyCode.LEFT (just an example, I want to handle every arrow key) event on StackPane.
As I am concerned the particular event is consumed on ScrollPane. I would like to prevent that but cannot find any reasonable way.
import javafx.application.Application;
import javafx.event.Event;
import javafx.event.EventDispatcher;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ScrollPaneDispatcherApp extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
StackPane stackPane = new StackPane();
stackPane.setStyle("-fx-backgound-color: red");
ScrollPane scrollPane = new ScrollPane();
scrollPane.setStyle("-fx-background-color: yellow");
stackPane.getChildren().add(scrollPane);
Scene scene = new Scene(stackPane);
stage.setScene(scene);
stage.show();
scrollPane.requestFocus();
EventDispatcher sceneEventDispatcher = scene.getEventDispatcher();
EventDispatcher stackPaneEventDispatcher = stackPane.getEventDispatcher();
EventDispatcher scrollPaneEventDispatcher = scrollPane.getEventDispatcher();
scene.setEventDispatcher((event, tail) -> {
if (KeyEvent.ANY.equals(event.getEventType().getSuperType())) {
System.out.println("DISPATCH\tScene\t\tevent=" + event.getEventType());
}
return sceneEventDispatcher.dispatchEvent(event, tail);
});
stackPane.setEventDispatcher((event, tail) -> {
if (KeyEvent.ANY.equals(event.getEventType().getSuperType())) {
System.out.println("DISPATCH\tStackPane\tevent=" + event.getEventType());
}
return stackPaneEventDispatcher.dispatchEvent(event, tail);
});
scrollPane.setEventDispatcher((event, tail) -> {
if (KeyEvent.ANY.equals(event.getEventType().getSuperType())) {
System.out.println("DISPATCH\tScrollPane\tevent=" + event.getEventType());
}
Event eventToDispatch = scrollPaneEventDispatcher.dispatchEvent(event, tail);
if (KeyEvent.KEY_PRESSED.equals(event.getEventType())) {
if (KeyCode.LEFT.equals(((KeyEvent) event).getCode()) || KeyCode.RIGHT.equals(((KeyEvent) event).getCode())) {
if (eventToDispatch == null) {
return event;
}
}
}
return eventToDispatch;
});
scene.addEventFilter(KeyEvent.ANY,
event -> System.out.println("FILTER\t\tScene\t\tevent=" + event.getEventType()));
stackPane.addEventFilter(KeyEvent.ANY,
event -> System.out.println("FILTER\t\tStackPane\tevent=" + event.getEventType()));
scrollPane.addEventFilter(KeyEvent.ANY,
event -> System.out.println("FILTER\t\tScrollPane\tevent=" + event.getEventType()));
scene.addEventHandler(KeyEvent.ANY,
event -> System.out.println("HANDLER\t\tScene\t\tevent=" + event.getEventType()));
stackPane.addEventHandler(KeyEvent.ANY,
event -> System.out.println("HANDLER\t\tStackPane\tevent=" + event.getEventType()));
scrollPane.addEventHandler(KeyEvent.ANY,
event -> System.out.println("HANDLER\t\tScrollPane\tevent=" + event.getEventType()));
}
}
Proposed solution overrides LEFT, RIGHT arrows KEY_PRESSED events consumption behaviour for ScrollPane. The clue is the new EventDispatcher for ScrollPane. Rest of the code is only for debugging purposes.
By default, the ScrollPane will consume the key press event and the StackPane will not.
If you want to intercept a key press using a parent pane event handler, then you should use an event filter (https://docs.oracle.com/javafx/2/events/filters.htm) to do so (to intercept the event during the capturing phase rather than the bubbling phase).
Read up on event processing (https://docs.oracle.com/javafx/2/events/processing.htm) if you need to understand the capturing versus bubbling concepts.
From docs.oracle.com/javafx/2/events/filters.htm : Event filters enable the parent node to provide common processing for its child nodes or to intercept an event and prevent child nodes from acting on the event

How to get JavaFX TreeView to behave consistently upon node expansion?

I have a JavaFX TreeView with an invisible root and a handful of 'folder' TreeItems that have many 'file' TreeItems as children. The 'folder' TreeItems typically fit inside the TreeView without there being any scrollbars.
invisible-root/
folder/
folder/
folder/
file
file
file
...
file
Sometimes, when I expand a 'folder' TreeItem, the scrollbars appear but the scroll position remains the same. (This is what I want!) However, sometimes, expanding a TreeItem causes the scrollbars appear and the TableView scrolls to the last child of the expanded TreeItem!
This is very unexpected and surprising, especially since I have difficulty predicting which of the two behaviors I will see: (1) stay put, or (2) scroll to last item. Personally, I think behavior (1) is less surprising and preferable.
Any thoughts on how to deal with this?
I see this behavior on Java8u31.
The problem is in VirtualFlow. In layoutChildren() there is this section:
if (lastCellCount != cellCount) {
// The cell count has changed. We want to keep the viewport
// stable if possible. If position was 0 or 1, we want to keep
// the position in the same place. If the new cell count is >=
// the currentIndex, then we will adjust the position to be 1.
// Otherwise, our goal is to leave the index of the cell at the
// top consistent, with the same translation etc.
if (position == 0 || position == 1) {
// Update the item count
// setItemCount(cellCount);
} else if (currentIndex >= cellCount) {
setPosition(1.0f);
// setItemCount(cellCount);
} else if (firstCell != null) {
double firstCellOffset = getCellPosition(firstCell);
int firstCellIndex = getCellIndex(firstCell);
// setItemCount(cellCount);
adjustPositionToIndex(firstCellIndex);
double viewportTopToCellTop = -computeOffsetForCell(firstCellIndex);
adjustByPixelAmount(viewportTopToCellTop - firstCellOffset);
}
The problem arises if position is 1.0 (== scrolled to bottom), because in that case there is no recalculation. A workaround would be to override the TreeViewSkin to provide your own VirtualFlow and fix the behavior there.
The code below is meant to illustrate the problem, it's not a real solution, just a starting point if you really want to fix it:
import com.sun.javafx.scene.control.skin.TreeViewSkin;
import com.sun.javafx.scene.control.skin.VirtualFlow;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class TreeViewScrollBehaviour extends Application {
#Override
public void start(Stage primaryStage) {
TreeView treeView = new TreeView() {
#Override
protected Skin createDefaultSkin() {
return new TTreeViewSkin(this); //To change body of generated methods, choose Tools | Templates.
}
};
TreeItem<String> treeItem = new TreeItem<String>("Root");
for (int i = 0; i < 20; i++) {
TreeItem<String> treeItem1 = new TreeItem<>("second layer " + i);
treeItem.getChildren().add(treeItem1);
for (int j = 0; j < 20; j++) {
treeItem1.getChildren().add(new TreeItem<>("Third Layer " + j));
}
}
treeView.setRoot(treeItem);
StackPane root = new StackPane();
root.getChildren().addAll(treeView);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
class TTreeViewSkin<T extends IndexedCell> extends TreeViewSkin<T> {
public TTreeViewSkin(TreeView treeView) {
super(treeView);
}
#Override
protected VirtualFlow createVirtualFlow() {
return new TVirtualFlow<T>(); //To change body of generated methods, choose Tools | Templates.
}
}
class TVirtualFlow<T extends IndexedCell> extends VirtualFlow<T> {
#Override
public double getPosition() {
double position = super.getPosition();
if (position == 1.0d) {
return 0.99999999999;
}
return super.getPosition(); //To change body of generated methods, choose Tools | Templates.
}
#Override
public void setPosition(double newPosition) {
if (newPosition == 1.0d) {
newPosition = 0.99999999999;
}
super.setPosition(newPosition); //To change body of generated methods, choose Tools | Templates.
}
}
}

Resources