How do I stop TextArea from listening to Shortcut KeyCombinations as KeyEvents? - javafx

Just as the title says, how do I stop shortcut keys (accelerators) being picked up as key events in TextArea? I have tried the method suggested here with different modifications: TextArea ignore KeyEvent in JavaFX with no luck.

If you want to stop specific accelerators from working when the TextArea has focus simply add an event filter for KEY_PRESSED events.
public class AcceleratorFilter implements EventHandler<KeyEvent> {
// blacklist of KeyCombinations
private final Set<KeyCombination> combinations;
public AcceleratorFilter(KeyCombination... combinations) {
this.combinations = Set.of(combinations);
}
#Override
public void handle(Event event) {
if (combinations.stream().anyMatch(combo -> combo.match(event)) {
event.consume();
}
}
}
TextArea area = new TextArea();
area.addEventFilter(KeyEvent.KEY_PRESSED, new AcceleratorFilter(
KeyCombination.valueOf("shortcut+o"),
KeyCombination.valueOf("shortcut+s") // etc...
));
If you want to indiscriminately block all accelerators registered with the Scene then you can query the Scenes accelerators and consume the KeyEvent if appropriate.
TextArea area = new TextArea();
area.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
var scene = ((Node) event.getSource()).getScene();
// #getAccelerators() = ObservableMap<KeyCombination, Runnable>
var combos = scene.getAccelerators().keySet();
if (combos.stream().anyMatch(combo -> combo.match(event)) {
event.consume();
}
});
This latter option may cause issues if you're not careful. For instance, if you have a default Button in the Scene then the above event filter may interfere with the ENTER key. Also, this option won't necessarily stop things like shortcut+c, shortcut+v, etc. because those shortcuts are registered with the TextInputControl, not the Scene.

Related

JavaFX tableview auto scroll to selected item when pressing a button to selectNext() or selectPrevious()

I'm writing a JavaFX program with a TableView called 'table' and 2 buttons called 'previous' & 'next'.
Here is part of the code:
previous.setOnAction(event -> {
table.getSelectionModel().selectPrevious();
});
next.setOnAction(event -> {
table.getSelectionModel().selectNext();
});
However, if I keep pressing the buttons, the table will not scroll automatically to keep the selected item visible. So I modified the code like this :
previous.setOnAction(event -> {
table.getSelectionModel().selectPrevious();
table.scrollTo(table.getSelectionModel().getSelectedItem());
});
next.setOnAction(event -> {
table.getSelectionModel().selectNext();
table.scrollTo(table.getSelectionModel().getSelectedItem());
});
But it will always try to keep the selected item at the top of the visible region. If I keep pressing 'next'. The selected item will stay at the top instead of staying at the bottom.
I want to mimic the natural behavior of a tableview in the way that if I press up or down on the keyboard with something selected, the tableview will scroll automatically to keep the selected item visible.
How should I modify the code to make the auto scrolling more natural when I press the buttons?
Thanks
The problem is
missing fine-grained control of scrollTo target location on application level
the (somewhat unfortunate) implementation of virtualizedControl.scrollTo(index) which (ultimately) leads to calling flow.scrollToTop(index)
There's a long-standing RFE (reported 2014!) requesting better control from application code. Actually, VirtualFlow has public methods (scrollToTop, scrollTo, scrollPixels) providing such, only they are not passed on to the control layer (getVirtualFlow in VirtualContainerBase is final protected), so can't be overridden in a custom skin. Since fx12, we can hack a bit, and expose the onSelectXX of Tree/TableViewSkin and use those, either directly in application code (example below) or in a custom TableView.
Example code:
public class TableSelectNextKeepVisible extends Application {
/**
* Custom table skin to expose onSelectXX methods for application use.
*/
public static class MyTableSkin<T> extends TableViewSkin<T> {
public MyTableSkin(TableView<T> control) {
super(control);
}
/**
* Overridden to widen scope to public.
*/
#Override
public void onSelectBelowCell() {
super.onSelectBelowCell();
}
/**
* Overridden to widen scope to public.
*/
#Override
public void onSelectAboveCell() {
super.onSelectAboveCell();
}
}
private Parent createContent() {
TableView<Locale> table = new TableView<>(FXCollections.observableArrayList(Locale.getAvailableLocales())) {
#Override
protected Skin<?> createDefaultSkin() {
return new MyTableSkin<>(this);
}
};
TableColumn<Locale, String> country = new TableColumn<>("Column");
country.setCellValueFactory(new PropertyValueFactory<>("displayLanguage"));
table.getColumns().addAll(country);
Button next = new Button("next");
next.setOnAction(e -> {
table.getSelectionModel().selectNext();
// scrolls to top
// table.scrollTo(table.getSelectionModel().getSelectedIndex());
((MyTableSkin<?>) table.getSkin()).onSelectBelowCell();
});
Button previous = new Button("previous");
previous.setOnAction(e -> {
table.getSelectionModel().selectPrevious();
// scrolls to top
// table.scrollTo(table.getSelectionModel().getSelectedIndex());
((MyTableSkin<?>) table.getSkin()).onSelectAboveCell();
});
BorderPane content = new BorderPane(table);
content.setBottom(new HBox(10, next, previous));
return content;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
try using getSelectedIndex as follows instead of using getSelectedItem
previous.setOnAction(event -> {
table.getSelectionModel().selectPrevious();
table.scrollTo(table.getSelectionModel().getSelectedIndex());
});
Platform.runLater( () -> TABLE_NAME.scrollTo(TABLE_INFORMATION_LIST.getList().size()-index) );
should work if you call it whenever you add information to the table.

Key Listener in JavaFX that changes on button press

My controller class has a moveButton method that on button click moves the button to a new location. This works fine and is called by a number of buttons which do the same thing. I want to add a key listener so when a button has been clicked once, until a different button is clicked, the user can use the up arrow to move the button (ie call the same moveButton function). The below is how I have tried to implement it, I also tried putting the key listener in the initialize method but neither seem to be working. Any advice would be greatly appreciated!
public void moveButton(ActionEvent actionEvent) {
Button buttonPressed = (Button) actionEvent.getSource();
double newAnchor = getNewAnchor(AnchorPane.getBottomAnchor(buttonPressed)) // separate method that returns new anchor location
AnchorPane.setBottomAnchor(buttonPressed, newAnchor);
buttonPressed.getScene().setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
if(event.getCode() == KeyCode.UP){
moveButton(actionEvent);
}
}
});
}
Don't treat the events like data that you need to pass around. Use them as triggers to do work. Generally, don't write generic event handlers that are called from multiple events and multiple nodes. Write short event handlers that just call methods to do something, and pass them the minimum from the event that they need to do the job.
If you do this, then it changes your thinking about how all of this stuff works and then it's just plain old Java, with no magic. And it's simple:
public class MoveButton extends Application {
private Node activeButton;
private Pane pane;
#Override
public void start(Stage primaryStage) throws Exception {
pane = new Pane();
Scene scene = new Scene(pane, 1200, 800);
primaryStage.setScene(scene);
primaryStage.show();
Button button1 = new Button("Button 1");
Button button2 = new Button("Button 2");
button2.setTranslateX(80);
button1.setOnAction(evt -> buttonClick(button1));
button2.setOnAction(evt -> buttonClick(button2));
pane.getChildren().addAll(button1, button2);
pane.setOnKeyPressed(evt -> moveButton(evt.getCode()));
}
private void moveButton(KeyCode keyCode) {
switch (keyCode) {
case UP -> activeButton.setTranslateY(activeButton.getTranslateY() - 30);
case RIGHT -> activeButton.setTranslateX(activeButton.getTranslateX() + 30);
case DOWN -> activeButton.setTranslateY(activeButton.getTranslateY() + 30);
case LEFT -> activeButton.setTranslateX(activeButton.getTranslateX() - 30);
}
}
private void buttonClick(Node button) {
activeButton = button;
pane.requestFocus();
}
}

JavaFX Events for Mouse Interactions are not triggered if Key is pressed

JavaFX does not execute events like the ActionEvent for Button or CheckBox, if a modifier key like CTRL or SHIFT is pressed. As far as I understand this behavior is implemented in ButtonBehavior (e.g. note the expression ! keyDown in the following method from that class):
#Override public void mouseReleased(MouseEvent e) {
// if armed by a mouse press instead of key press, then fire!
final ButtonBase button = getControl();
if (! keyDown && button.isArmed()) {
button.fire();
button.disarm();
}
}
First of all, I do not really understand the reason for this. What is the purpose of not firing a button if a key is pressed?
This is my use-case: I want to implement a checkbox that can be checked/unchecked as normal. It will toggle some state in a model. But it should have an additional feature: if the user presses some key like CTRL while checking/unchecking with the mouse, an additional flag called "locked" or "protected" should be set in the model, which will prevent that the state can be overwritten by some other logic of the application.
This should give an idea about the use-case, but if not it doesn't really matter for my actual question: How can I make it possible that a CheckBox can still be toggled (or a Button be pressed) even though the user presses a modifier key?
Thanks for your help!
That is odd you can implement it yourself like so
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
VBox vBox = new VBox();
vBox.setAlignment(Pos.CENTER);
CheckBox checkBox = new CheckBox();
checkBox.setOnMouseClicked(event -> {
if(event.isControlDown()) {
System.out.print("Control down click ");
checkBox.setSelected(!checkBox.isSelected());
}
else
System.out.print("Normal click ");
System.out.println("Checked Status:"+checkBox.isSelected());
});
Button button = new Button("Button");
button.setOnMouseClicked(event -> {
if(event.isControlDown())
System.out.println("Control down click");
else
System.out.println("Normal click");
});
vBox.getChildren().addAll(new Label("Click the box"),checkBox,button);
primaryStage.setScene(new Scene(vBox));
primaryStage.show();
}
public static void main(String[] args) { launch(args); }
}
The output for CheckBox:
Normal click Checked Status:true
Normal click Checked Status:false
Control down click Checked Status:true
Control down click Checked Status:false
The output for Button:
Normal click
Control down click

javafx have an eventfilter remove itself

A snippet of my code is as follows:
//button clicked method
#FXML
public void newHeatExchanger2ButtonClicked(ActionEvent event) throws Exception {
//new pane created
Pane pane = new Pane();
//method call everytime the button is clicked
create2DExchanger(pane);
}
//method declaration
private void create2DExchanger(Pane pane) {
EventHandler<MouseEvent> panePressed = (e -> {
if (e.getButton() == MouseButton.SECONDARY){
do stuff
}
if (e.getButton() == MouseButton.PRIMARY){
do stuff
}
});
EventHandler<MouseEvent> paneDragged = (e -> {
if (e.getButton() == MouseButton.PRIMARY){
do stuff
}
});
EventHandler<MouseEvent> paneReleased = (e -> {
if (e.getButton() == MouseButton.PRIMARY){
do stuff;
}
});
EventHandler<MouseEvent> paneMoved = (t -> {
do stuff;
});
EventHandler<MouseEvent> paneClicked = (t -> {
//I need this filter to remove itself right here
t.consume();
pane.removeEventFilter(MouseEvent.MOUSE_MOVED, paneMoved);
pane.addEventHandler(MouseEvent.MOUSE_PRESSED, panePressed);
pane.addEventHandler(MouseEvent.MOUSE_DRAGGED, paneDragged);
pane.addEventHandler(MouseEvent.MOUSE_RELEASED, paneReleased);
});
pane.removeEventHandler(MouseEvent.MOUSE_PRESSED, panePressed);
pane.removeEventHandler(MouseEvent.MOUSE_DRAGGED, paneDragged);
pane.removeEventHandler(MouseEvent.MOUSE_RELEASED, paneReleased);
pane.addEventFilter(MouseEvent.MOUSE_MOVED, paneMoved);
pane.addEventFilter(MouseEvent.MOUSE_PRESSED, paneClicked);
}
Initially I set the pane to have only the event filters of mouse_moved, and mouse_pressed. As soon as the mouse is clicked I need the mouse filter for mouse_pressed and mouse_moved to go away and add the eventHandlers as I do in the paneClicked filter llamda. I need the first set of events to be filters because there are children nodes I do not want to receive the event (i.e. an event filter on an arc that is a child of the pane). The second set need to be handlers because the arc event filter needs to consume the event before the pane eventHandlers receive it.
Convenience events like:
pane.setOnMousePressed()
can remove themselves by calling
pane.setOnMousePressed(null);
but I need this initial event filter to remove itself. I will need the functionality of an event removing itself later as well in the code, but if I try to add
pane.removeEventFilter(MouseEvent.MOUSE_PRESSED, paneClicked);
to
EventHandler<MouseEvent> paneClicked = (t -> {
//I need this filter to remove itself right here
t.consume();
pane.removeEventFilter(MouseEvent.MOUSE_MOVED, paneMoved);
pane.addEventHandler(MouseEvent.MOUSE_PRESSED, panePressed);
pane.addEventHandler(MouseEvent.MOUSE_DRAGGED, paneDragged);
pane.addEventHandler(MouseEvent.MOUSE_RELEASED, paneReleased);
});
It will not compile. I have been researching for a couple of days on how to get the functionality of an eventFilter or eventHandler to remove itself but I am coming up short. I have not found anything online or on stackexchange in my Google searches either. I would really appreciate being able to figure this thing out. Thanks.
I believe the problem stems from the fact that you try to access paneClicked before it was fully declared.
You can overcome this using an anonymous class with the this keyword:
EventHandler<MouseEvent> paneClicked = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
event.consume();
pane.removeEventFilter(MouseEvent.MOUSE_PRESSED, this);
pane.removeEventFilter(MouseEvent.MOUSE_MOVED, paneMoved);
pane.addEventHandler(MouseEvent.MOUSE_PRESSED, panePressed);
pane.addEventHandler(MouseEvent.MOUSE_DRAGGED, paneDragged);
pane.addEventHandler(MouseEvent.MOUSE_RELEASED, paneReleased);
}
};
Or by referencing a fully qualified static function:
public class ContainingClass {
...
private static EventHandler<MouseEvent> paneClicked = (t -> {
t.consume();
pane.removeEventFilter(
MouseEvent.MOUSE_PRESSED, ContainingClass.paneClicked
);
});
}
See also:
Why do lambdas in Java 8 disallow forward reference to member variables where anonymous classes don't?
Java8 Lambdas vs Anonymous classes

How to get a JavaFX MenuItem to respond to a TAB KeyPress?

A JavaFX MenuItem can respond to most KeyPress events by setting an ActionEvent EventHandler. However, while the event handler does catch a KeyPress of KeyCode.ENTER, it does not catch a KeyCode.TAB KeyPress event. Apparently, some key events like TAB are handled at a deeper level. For example, the arrow keys enable traversal of the menu.
My ContextMenu is a list of completions of an email address string the user has started typing in a TextField. The users want to press the arrow keys to select the desired item, and the TAB key to execute the completion.
I can attach an event handler to the ContextMenu itself and catch the TAB keypress. But the event's Source is then the ContextMenu, and I can find no variables in the ContextMenu indicating which MenuItem was highlighted when the TAB key was pressed. MenuItem allows css style to control appearance of the menu item in focus, but it does not have any properties telling whether it is in focus or not.
I have tried futzing with the EventDispatchChain via MenuItem buildEventDispatchChain() to no avail. There seems to be no way to intercept the TAB KeyPress or otherwise determine which menu item was in focus when the TAB key was pressed.
Any suggestions?
If I get this right, you want to override the default keypressed listener to add your own response, so for that we have to find where it's applied.
To get this working, we've got to get our hands dirty with private API...
ContextMenu skin (ContextMenuSkin) uses a ContextMenuContent object, as a container with all the items. Each of these items are also in a ContextMenuContent.MenuItemContainer container.
We can override the keypressed listener on the parent container, while we can add a focusedProperty listener to the items on the items container.
Using this private API
import com.sun.javafx.scene.control.skin.ContextMenuContent;
this is working for me:
private ContextMenuContent.MenuItemContainer itemSelected=null;
#Override
public void start(Stage primaryStage) {
MenuItem cmItem1 = new MenuItem("Item 1");
cmItem1.setOnAction(e->System.out.println("Item 1"));
MenuItem cmItem2 = new MenuItem("Item 2");
cmItem2.setOnAction(e->System.out.println("Item 2"));
final ContextMenu cm = new ContextMenu(cmItem1,cmItem2);
Scene scene = new Scene(new StackPane(), 300, 250);
scene.setOnMouseClicked(t -> {
if(t.getButton()==MouseButton.SECONDARY || t.isControlDown()){
cm.show(scene.getWindow(),t.getScreenX(),t.getScreenY());
ContextMenuContent cmc= (ContextMenuContent)cm.getSkin().getNode();
cmc.setOnKeyPressed(ke->{
switch (ke.getCode()) {
case UP: break;
case DOWN: break;
case TAB: ke.consume();
if(itemSelected!=null){
itemSelected.getItem().fire();
}
cm.hide();
break;
default: break;
}
});
VBox itemsContainer = cmc.getItemsContainer();
itemsContainer.getChildren().forEach(n->{
ContextMenuContent.MenuItemContainer item=(ContextMenuContent.MenuItemContainer)n;
item.focusedProperty().addListener((obs,b,b1)->{
if(b1){
itemSelected=item;
}
});
});
}
});
primaryStage.setScene(scene);
primaryStage.show();
}
Excellent! Thank you #jose! I ended up writing somewhat different code but
the key is using com.sun.javafx.scene.control.skin.ContextMenuContent, which provides
access to the ContextMenuContent.MenuItemContainer objects that hold the MenuItems.
In order to not break the existing UP/DOWN key behavior, I added a new handler
to the ContextMenuContent object; this handler only consumes the TAB KeyPress and
everthing else passes through to their normal handlers.
Looking at the ContextMenuContent class, I borrowed their existing method for
finding the focused item, so didn't have to add focusedProperty listeners.
Also, I'm on Java 1.7 and don't have lambdas and I use a very basic programming style.
public class MenuItemHandler_CMC <T extends Event> implements EventHandler {
public ContextMenuContent m_cmc;
public AddressCompletionMenuItemHandler_CMC(ContextMenuContent cmc){
m_cmc = cmc;
}
#Override
public void handle(Event event){
KeyEvent ke = (KeyEvent)event;
switch(ke.getCode()){
case TAB:
ke.consume();
MenuItem focused_menu_item = findFocusedMenuItem();
if(focused_menu_item != null){
focused_menu_item.fire();
}
break;
default: break;
}
}
public MenuItem findFocusedMenuItem() {
VBox items_container = m_cmc.getItemsContainer();
for (int i = 0; i < items_container.getChildren().size(); i++) {
Node n = items_container.getChildren().get(i);
if (n.isFocused()) {
ContextMenuContent.MenuItemContainer menu_item_container = (ContextMenuContent.MenuItemContainer)n;
MenuItem menu_item = menu_item_container.getItem();
return menu_item;
}
}
return null;
}
}
...Attach the additional handler
if(m_context_menu.getSkin() != null){
ContextMenuContent cmc = (ContextMenuContent)m_context_menu.getSkin().getNode();
MenuItemHandler_CMC menu_item_handler_cmc = new MenuItemHandler_CMC(cmc);
cmc.addEventHandler(KeyEvent.KEY_PRESSED, menu_item_handler_cmc);
}

Resources