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
Related
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);
}
I have a problem with JavaFx ListView component. I'm using popup with TextField and ListView inside of VBox. When TextField is in focus, I can normally close this popup window pressing the Esc key on the keyboard, but when ListView item is in focus popup stays open, nothing happens.
Minimal reproducible example:
package sample;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
MenuItem rightClickItem = new MenuItem("CLICK!");
rightClickItem.setOnAction(a -> showdialog());
ContextMenu menu = new ContextMenu(rightClickItem);
Label text = new Label("Right Click on me");
text.setContextMenu(menu);
StackPane root = new StackPane(text);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("RightClick MenuItem And Dialog");
primaryStage.setScene(scene);
primaryStage.show();
}
private void showdialog() {
Dialog<ButtonType> dialog = new Dialog<>();
dialog.getDialogPane().getButtonTypes().add(ButtonType.CANCEL);
VBox vBox = new VBox();
ListView listView = new ListView();
listView.getItems().add("Item 1");
listView.getItems().add("Item 2");
vBox.getChildren().add(new TextField());
vBox.getChildren().add(listView);
vBox.addEventHandler(KeyEvent.KEY_PRESSED, keyEvent -> System.err.println("Key pressed: " + keyEvent.getCode()));
dialog.getDialogPane().setContent(vBox);
dialog.showAndWait();
}
public static void main(String[] args) {
launch(args);
}
}
It seems to me that Esc key is consumed in ListView, and this cause a problem with closing a popup.
Just to mention, I'm using zulu-11.0.8 JDKFx version.
It seems to me that Esc key is consumed in ListView, and this cause a problem with closing a popup.
That's indeed the problem - happens with all controls that have a consuming KeyMapping to ESCAPE added by their respective Behavior (f.i. also for a TextField with TextFormatter).
There is no clean way to interfere with it (Behavior and InputMap didn't yet make to move into public api). The way to hack around is to remove the KeyMapping from the Behavior's inputMap. Beware: you must be allowed to go dirty, that is use internal api and use reflection!
The steps:
grab the control's skin (available after the control is added to the scenegraph)
reflectively access the skin's behavior
remove the keyMapping from the behavior's inputMap
Example code snippet:
private void tweakInputMap(ListView listView) {
ListViewSkin<?> skin = (ListViewSkin<?>) listView.getSkin();
// use your favorite utility method to reflectively access the private field
ListViewBehavior<?> listBehavior = (ListViewBehavior<?>) FXUtils.invokeGetFieldValue(
ListViewSkin.class, skin, "behavior");
InputMap<?> map = listBehavior.getInputMap();
Optional<Mapping<?>> mapping = map.lookupMapping(new KeyBinding(KeyCode.ESCAPE));
map.getMappings().remove(mapping.get());
}
It's usage:
listView.skinProperty().addListener(ov -> {
tweakInputMap(listView);
});
To avoid using private API, you can use an event filter that, if the ListView is not editing, copies the Escape key event and fires it on the parent. From there, the copied event can propagate to be useful in other handlers such as closing a popup.
Also, if you need this on all ListViews in your application, you can do it in a derived class of ListViewSkin and set that as the -fx-skin for .list-view in your CSS file.
listView.addEventFilter( KeyEvent.KEY_PRESSED, keyEvent -> {
if( keyEvent.getCode() == KeyCode.ESCAPE && !keyEvent.isAltDown() && !keyEvent.isControlDown()
&& !keyEvent.isMetaDown() && !keyEvent.isShiftDown()
) {
if( listView.getEditingIndex() == -1 ) {
// Not editing.
final Parent parent = listView.getParent();
parent.fireEvent( keyEvent.copyFor( parent, parent ) );
}
keyEvent.consume();
}
} );
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.
I am trying to implement onMouseEnter and onMouseExit events on a JavaFX ListView. What I want to do is if the mouse moves over a node of the list view, I want to change the background color of the nodes that are currently visible children in the current view.
This post has a great code sample, but is not quite what I am looking for.
Apply style to TreeView children nodes in javaFX
Using that code as a reference, what I am looking for is a given tree:
Root -> Item: 1 -> Item: 100 -> Item 1000, Item 1001, Item 1002, Item 1003
When I mouse over "Item: 100" I would like it and Item 1000* to have a background color change.
This seems difficult to me because the getNextSibling and getPreviousSibling interface is on the TreeItem and though you can get a TreeItem from a TreeCell on the MouseEvent, you can't (that I know of) update CSS on a TreeItem and have it take effect in a TreeCell -- because the setStyle method is on the TreeCell.
Suggestions on how this can be done?
[Update note: I originally had a solution using a subclass of TreeItem. The solution presented here is much cleaner than the original.]
Create an ObservableSet<TreeItem<?>> containing the TreeItems that should be highlighted. Then in the cell factory, observe that set, and the cell's treeItemProperty(), and set the style class (I used a PseudoClass in the example below) so the cell is highlighted if the tree item belonging to the cell is in the set.
Finally, register mouseEntered and mouseExited handlers with the cell. When the mouse enters the cell, you can get the tree item, use it to navigate to any other tree items you need, and add the appropriate items to the set you defined. In the mouseExited handler, clear the set (or perform other logic as needed).
import java.util.HashSet;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class HighlightingTree extends Application {
private final PseudoClass highlighted = PseudoClass.getPseudoClass("highlighted");
#Override
public void start(Stage primaryStage) {
TreeView<Integer> tree = new TreeView<>();
tree.setRoot(buildTreeRoot());
ObservableSet<TreeItem<Integer>> highlightedItems = FXCollections.observableSet(new HashSet<>());
tree.setCellFactory(tv -> {
// the cell:
TreeCell<Integer> cell = new TreeCell<Integer>() {
// indicates whether the cell should be highlighted:
private BooleanBinding highlightCell = Bindings.createBooleanBinding(() ->
getTreeItem() != null && highlightedItems.contains(getTreeItem()),
treeItemProperty(), highlightedItems);
// listener for the binding above
// note this has to be scoped to persist alongside the cell, as the binding
// will use weak listeners, and we need to avoid the listener getting gc'd:
private ChangeListener<Boolean> listener = (obs, wasHighlighted, isHighlighted) ->
pseudoClassStateChanged(highlighted, isHighlighted);
// anonymous constructor: register listener with binding
{
highlightCell.addListener(listener);
}
};
// display correct text:
cell.itemProperty().addListener((obs, oldItem, newItem) -> {
if (newItem == null) {
cell.setText(null);
} else {
cell.setText(newItem.toString());
}
});
// mouse listeners:
cell.setOnMouseEntered(e -> {
if (cell.getTreeItem() != null) {
highlightedItems.add(cell.getTreeItem());
highlightedItems.addAll(cell.getTreeItem().getChildren());
}
});
cell.setOnMouseExited(e -> highlightedItems.clear());
return cell ;
});
BorderPane uiRoot = new BorderPane(tree);
Scene scene = new Scene(uiRoot, 600, 600);
scene.getStylesheets().add("highlight-tree-children.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private TreeItem<Integer> buildTreeRoot() {
return buildTreeItem(1);
}
private TreeItem<Integer> buildTreeItem(int n) {
TreeItem<Integer> item = new TreeItem<>(n);
if (n < 10_000) {
for (int i = 0; i<10; i++) {
item.getChildren().add(buildTreeItem(n * 10 + i));
}
}
return item ;
}
public static void main(String[] args) {
launch(args);
}
}
highlight-tree-children.css:
.tree-cell:highlighted {
-fx-background: yellow ;
}
table.setOnKeyPressed(new EventHandler<KeyEvent>() {
// final KeyCombination kb = new KeyCodeCombination(KeyCode.P, KeyCombination.CONTROL_DOWN);
// final KeyCombination k = new KeyCodeCombina
public void handle(KeyEvent key) {
if (key.getCode() == KeyCode.P && key.isControlDown()) {
//My Code
}
}
});
I want to invoke the event with the shortcut keycombination of Ctrl+P+X
It is actually a little hard to understand what Ctrl+P+X means. I am going to assume it means that you press ctrl, then you press p, then you press x (potentially releasing the p before you press the x). I'll also assume that the order matters, e.g. press ctrl, then press x then press p would not count. Anyway a bit of speculation on my part, perhaps not exactly what you want, but hopefully you will get the gist of the provided solution and be able to adapt it to your situation.
The solution monitors both key presses and releases so that it can keep track of the state of key presses to determine if the key combination triggers.
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.time.LocalTime;
public class KeyCombo extends Application {
KeyCombination ctrlP = KeyCodeCombination.keyCombination("Ctrl+P");
KeyCombination ctrlX = KeyCodeCombination.keyCombination("Ctrl+X");
#Override
public void start(Stage stage) throws Exception {
Label lastPressedLabel = new Label();
TextField textField = new TextField();
BooleanProperty pDown = new SimpleBooleanProperty(false);
textField.setOnKeyPressed(event -> {
if (ctrlP.match(event)) {
pDown.set(true);
}
if (pDown.get() && ctrlX.match(event)) {
pDown.set(false);
lastPressedLabel.setText(
LocalTime.now().toString()
);
}
});
textField.setOnKeyReleased(event -> {
if (!event.isControlDown()) {
pDown.set(false);
}
});
VBox layout = new VBox(10,
new Label("Press Ctrl+P+X"),
textField,
lastPressedLabel
);
layout.setPadding(new Insets(10));
Scene scene = new Scene(layout);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
If you can, I'd advise trying to use a simpler control scheme, e.g. just Ctrl+P or Ctrl+X (which is directly supported by the key code combination event matching), rather than using a composite control scheme of Ctrl+P+X.