Key Listener in JavaFX that changes on button press - javafx

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();
}
}

Related

How to prevent firing ActionEvent when ComboBox value property is changed programmatically?

I'm working with ComboBox control and I want to do something easy with it. I want ComboBox to fire an ActionEvent when its value is changed during mouse click on the ComboBox dropdown list (This is automatically done). In the opposite side, I want ComboBox Not to fire ActionEvent when its value is changed programmatically (e.g. when using comboBox.getSelectionModel().selectFirst()).
Here is a simple code to demonstrate the problem:
#Override
public void start(Stage primaryStage) {
VBox vBox = new VBox();
ComboBox<String> comboBox = new ComboBox<>();
comboBox.setItems(FXCollections.observableArrayList("John", "Josh", "Mosh"));
comboBox.setOnAction(event -> {
System.out.println("Action");
});
Task<Void> task = new Task<Void>() {
#Override
protected Void call() throws Exception {
return null;
}
};
task.setOnSucceeded(e -> comboBox.getSelectionModel().select("John"));
new Thread(task).start();
vBox.getChildren().addAll(comboBox);
vBox.setPrefWidth(200);
vBox.setPrefHeight(200);
vBox.setAlignment(Pos.CENTER);
Scene scene = new Scene(vBox);
Stage stage = new Stage();
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
As you can see, ComboBox has a setOnAction method which should be invoked only when ComboBox value is changed by a mouse click on the dropdown list. Also, there is a Task that does some operations. (Those operations are omitted for code simplicity reasons). After the Task is completed successfully, the ComboBox's value changes and setOnAction method is invoked too, while the value should be changed without invoking setOnAction method. I don't know how to achieve this. Any useful suggestions or tips are greatly appreciated.
I have successfully used Slaw's approach to suppress event firing across multiple controls at once -
set up a boolean field
private boolean programmedAction = false;
in the programmatic method updating the control, set the flag first to suppress actions
private void someMethod(){
programmedAction = true;
// manipulate controls
programmedAction = false;
}
in the control related events, check the boolean before firing
private void someControlsAction(ActionEvent actionEvent) {
if (programmedAction) return;
// do regular action stuff
}

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

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.

JavaFX secondary stage onCloseRequest

I have a little issue close a secondary stage after clicking the close at the top right corner.
I'm using fxml with controller class, i need a way to handle this situation.
Here is what i do but i get a nullpointer exception :
#Override
public void initialize(URL location, ResourceBundle resources) {
Stage stage = (Stage) tbTabPaneHome.getScene().getWindow();
stage.setOnCloseRequest(e -> {
Platform.exit();
System.exit(0);
});
}
Because the stage not yet intialized completly, so any other ideas ?
Since the Scene nor the Stage are created yet, you can't call them or you get a NPE, as you already mentioned.
One way to install the event handler on the stage will be listening to changes in the sceneProperty() of tbTabPaneHome.
Once the node is added to the scene, that property will give you the Scene instance.
But the scene is not added to the Stage yet, so you need to wait till this is done, with Platform.runLater():
public void initialize() {
tbTabPaneHome.sceneProperty().addListener((obs, oldScene, newScene) -> {
Platform.runLater(() -> {
Stage stage = (Stage) newScene.getWindow();
stage.setOnCloseRequest(e -> {
Platform.exit();
System.exit(0);
});
});
});
}
Did you try to deal with your secondary stage entirely in the main stage controller?
I want to hide or show a help windows from a button or help menu in my main application controller. Something like the following:
public Button helpBtn;
Stage anotherStage = new Stage();
boolean secondaryInitialyzed = false;
boolean secondaryShowing = false;
public void showOrHideHelp(ActionEvent actionEvent) throws IOException {
if (!secondaryInitialyzed){
Parent anotherRoot = FXMLLoader.load(getClass().getResource("mySecondaryStage.fxml"));
anotherStage.setTitle("Secondary stage");
Scene anotherScene = new Scene(anotherRoot, 500, 350);
anotherStage.setScene(anotherScene);
secondaryInitialyzed = true;
}
if (secondaryShowing){
anotherStage.hide();
secondaryShowing = false;
helpBtn.setText("Show Help");
}
else {
anotherStage.show();
secondaryShowing = true;
helpBtn.setText("Hide Help");
}
It does work, and there might be a way for you to handle your setOnCloseRequest within the main controller.
I have the opposing issue, i.e preventing closing the secondary stage window by clicking the close at the top right corner. I'll look into setOnCloseRequest and see if there is a way there.
I also have an other unrelated problem: can I position the secondary in reference to the primary one?

JavaFX disable button

I'm writing a program in netbeans with javaFX
The view has several buttons in it with some bad buttons(like bombs is minesweeper), I'm trying to freeze the program when a bad button is pushed but i don't find how to do it
thanks!
There are various solutions to your problem. 2 among them are simply ignoring the action event or disabling the buttons like this:
public class ButtonAction extends Application {
final BooleanProperty buttonActionProperty = new SimpleBooleanProperty();
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) {
FlowPane root = new FlowPane();
CheckBox checkBox = new CheckBox( "Enabled");
checkBox.setSelected(true);
// solution 1: check if action is allowed and process it or not
buttonActionProperty.bind( checkBox.selectedProperty());
Button button = new Button( "Click Me");
button.setOnAction(e -> {
if( buttonActionProperty.get()) {
System.out.println( "Allowed, processing action");
} else {
System.out.println( "Not allowed, no action");
}
});
// solution 2: remove comments to activate the code
// button.disableProperty().bind(buttonActionProperty.not());
root.getChildren().addAll(checkBox, button);
primaryStage.setScene(new Scene(root, 600, 200));
primaryStage.show();
}
}
Add a ROOT typed event filter that consumes all kind of events (mouse, keyboard etc.)
btnThatHasHiddenMine.setOnAction(( ActionEvent event ) ->
{
System.out.println("Ohh no! You just stepped over the mine!");
getGameboardPane().addEventFilter( EventType.ROOT, Event::consume );
});
Add the filter to your GameboardPane only, since we don't want to freeze other part of the app.

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