Edit: I've cut down Mainboard to just the relevant function to make code shorter to read. If any additional code needs to be read to help understanding, please say
I'm currently making a multiplayer Tetris game which launches a start window with a button that, when pressed, creates two windows which both run an instance of Tetris.Each has its own inputs (arrow keys and wasd). Both instances work individually and respond only to their respective key inputs, but I have to click between them in order to switch which one receives inputs - I'd like to modify the program to dynamically switch between the two stages based on which key input is received.
/**
* Defines actions to take for each input.
* @param tetr, the current Tetromino.
*/
public void moveOnKeyPress() {
if(scene != null) {
scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
public void handle(KeyEvent event) {
if (player == 0) {
switch(event.getCode()) {
case UP:
tetr.rotate(MESH);
break;
case LEFT:
tetr.move(false, MESH);
break;
case RIGHT:
tetr.move(true, MESH);
break;
}
} else {
switch(event.getCode()) {
case W:
tetr.rotate(MESH);
break;
case A:
tetr.move(false, MESH);
break;
case D:
tetr.move(true, MESH);
break;
}
}
}
});
}
}
My plan is to modify this input handling method in the Tetris game class (Mainboard), as shown above, such that it takes a key event as a parameter in Mainboard while the handler is done within the class for the starting window (Startboard) and passed into the appropriate Mainboard. When trying to code this, however, I also had to click on Startboard to give it focus and the code failed to execute when trying to alter its modality to be the sole focused window. I'll include the code for Startboard below, I'm hoping someone can either tell me how to manually switch focus, help me understand why changing StartBoard's modality causes the code to stop functioning or can otherwise offer a solution to this problem please.
public final class StartBoard extends Application {
public static final int SIZE = 25;
public static final int XMAX = SIZE * 12;
public static final int YMAX = SIZE * 24;
private static Pane pane = new Pane();
private static Scene scene = new Scene(pane, XMAX, YMAX);
public static int highScore = 0;
private Stage stage;
private Button startButton = new Button("Start");
public static List<Stage> stageList = new ArrayList<Stage>();
public static List<MainBoard> gameList = new ArrayList<MainBoard>();
private AtomicInteger playerVal = new AtomicInteger(0);
static Text highScoreText = new Text("Highscore: " + highScore);
/**
* Launches the program and calls the start function.
* @param args
*/
public static void main(String[] args) {
launch(args);
}
/**
* Overridden function that's automatically called by the launch function.
* Sets up main stage and assigns an event to the start button.
*/
@Override
public void start(Stage stage) throws Exception {
this.stage = stage;
highScoreText.setStyle("-fx-font-size: 20; -fx-font-family: Arial;");
highScoreText.setY(50);
pane.getChildren().addAll(startButton, highScoreText);
stage.setScene(scene);
stage.setTitle("T E T R I S");
stage.show();
startButton.setOnAction(new EventHandler <ActionEvent>()
{
public void handle(ActionEvent event)
{
playerVal.set(0);
startTask();
}
});
}
/**
* Overwrites the global high score in the main window.
* @param score, higher score to overwrite current high score.
*/
public static void updateHighScore(int score) {
highScore = score;
highScoreText.setText("Highscore: " + highScore);
}
/**
* Creates a runnable task and threads to run it.
*/
public void startTask() {
//Creates runnable
Runnable task = new Runnable() {
public void run() {
runTask();
}
};
//Run task in bgThread
Thread bgThread = new Thread(task);
Thread bgThread2 = new Thread(task);
//Terminate running thread if application exists
bgThread.setDaemon(true);
bgThread2.setDaemon(true);
//Start thread
bgThread.start();
bgThread2.start();
}
/**
* Sets up new stages for a Tetris game.
*/
public void runTask() {
Platform.runLater(new Runnable()
{
@Override
public void run()
{
Pane localPane = new Pane();
Scene localScene = new Scene(localPane, XMAX + 150, YMAX);
Stage inner = new Stage(){{setScene(localScene); }};
inner.initOwner(stage);
//inner.initModality(Modality.WINDOW_MODAL);
int val = playerVal.getAndIncrement();
inner.setX((XMAX + 150) * val );
inner.setY(YMAX / 2);
stageList.add(inner);
inner.show();
MainBoard localMainBoard = new MainBoard(val);
gameList.add(localMainBoard);
try {
localMainBoard.start(inner);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
}
Edit: I've cut down Mainboard to just the relevant function to make code shorter to read. If any additional code needs to be read to help understanding, please say
I'm currently making a multiplayer Tetris game which launches a start window with a button that, when pressed, creates two windows which both run an instance of Tetris.Each has its own inputs (arrow keys and wasd). Both instances work individually and respond only to their respective key inputs, but I have to click between them in order to switch which one receives inputs - I'd like to modify the program to dynamically switch between the two stages based on which key input is received.
/**
* Defines actions to take for each input.
* @param tetr, the current Tetromino.
*/
public void moveOnKeyPress() {
if(scene != null) {
scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
public void handle(KeyEvent event) {
if (player == 0) {
switch(event.getCode()) {
case UP:
tetr.rotate(MESH);
break;
case LEFT:
tetr.move(false, MESH);
break;
case RIGHT:
tetr.move(true, MESH);
break;
}
} else {
switch(event.getCode()) {
case W:
tetr.rotate(MESH);
break;
case A:
tetr.move(false, MESH);
break;
case D:
tetr.move(true, MESH);
break;
}
}
}
});
}
}
My plan is to modify this input handling method in the Tetris game class (Mainboard), as shown above, such that it takes a key event as a parameter in Mainboard while the handler is done within the class for the starting window (Startboard) and passed into the appropriate Mainboard. When trying to code this, however, I also had to click on Startboard to give it focus and the code failed to execute when trying to alter its modality to be the sole focused window. I'll include the code for Startboard below, I'm hoping someone can either tell me how to manually switch focus, help me understand why changing StartBoard's modality causes the code to stop functioning or can otherwise offer a solution to this problem please.
public final class StartBoard extends Application {
public static final int SIZE = 25;
public static final int XMAX = SIZE * 12;
public static final int YMAX = SIZE * 24;
private static Pane pane = new Pane();
private static Scene scene = new Scene(pane, XMAX, YMAX);
public static int highScore = 0;
private Stage stage;
private Button startButton = new Button("Start");
public static List<Stage> stageList = new ArrayList<Stage>();
public static List<MainBoard> gameList = new ArrayList<MainBoard>();
private AtomicInteger playerVal = new AtomicInteger(0);
static Text highScoreText = new Text("Highscore: " + highScore);
/**
* Launches the program and calls the start function.
* @param args
*/
public static void main(String[] args) {
launch(args);
}
/**
* Overridden function that's automatically called by the launch function.
* Sets up main stage and assigns an event to the start button.
*/
@Override
public void start(Stage stage) throws Exception {
this.stage = stage;
highScoreText.setStyle("-fx-font-size: 20; -fx-font-family: Arial;");
highScoreText.setY(50);
pane.getChildren().addAll(startButton, highScoreText);
stage.setScene(scene);
stage.setTitle("T E T R I S");
stage.show();
startButton.setOnAction(new EventHandler <ActionEvent>()
{
public void handle(ActionEvent event)
{
playerVal.set(0);
startTask();
}
});
}
/**
* Overwrites the global high score in the main window.
* @param score, higher score to overwrite current high score.
*/
public static void updateHighScore(int score) {
highScore = score;
highScoreText.setText("Highscore: " + highScore);
}
/**
* Creates a runnable task and threads to run it.
*/
public void startTask() {
//Creates runnable
Runnable task = new Runnable() {
public void run() {
runTask();
}
};
//Run task in bgThread
Thread bgThread = new Thread(task);
Thread bgThread2 = new Thread(task);
//Terminate running thread if application exists
bgThread.setDaemon(true);
bgThread2.setDaemon(true);
//Start thread
bgThread.start();
bgThread2.start();
}
/**
* Sets up new stages for a Tetris game.
*/
public void runTask() {
Platform.runLater(new Runnable()
{
@Override
public void run()
{
Pane localPane = new Pane();
Scene localScene = new Scene(localPane, XMAX + 150, YMAX);
Stage inner = new Stage(){{setScene(localScene); }};
inner.initOwner(stage);
//inner.initModality(Modality.WINDOW_MODAL);
int val = playerVal.getAndIncrement();
inner.setX((XMAX + 150) * val );
inner.setY(YMAX / 2);
stageList.add(inner);
inner.show();
MainBoard localMainBoard = new MainBoard(val);
gameList.add(localMainBoard);
try {
localMainBoard.start(inner);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
}
Share
Improve this question
edited Mar 15 at 14:55
Kyle Nickson
asked Mar 14 at 6:14
Kyle NicksonKyle Nickson
11 silver badge1 bronze badge
4
|
1 Answer
Reset to default 1Preface: The examples in this answer don't build off your existing code. However, the concepts they demonstrate should be helpful and you should be able to apply said concepts to your own code.
To answer your fundamental question directly: Yes, it's possible to switch which window has focus based on key presses. It's relatively straightforward if you only want to handle key presses when at least one of your application's windows already has focus. One option is to add key-pressed filters to each window involved in the process. For example:
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage stageA) {
var labelA = new Label();
stageA.setScene(new Scene(new StackPane(labelA), 300, 150));
stageA.setTitle("Window A");
var labelB = new Label();
var stageB = new Stage();
stageB.setScene(new Scene(new StackPane(labelB), 300, 150));
stageB.setTitle("Window B");
var text = Bindings.createStringBinding(
() -> {
if (stageA.isFocused()) return "\"Window A\" is focused!";
else if (stageB.isFocused()) return "\"Window B\" is focused!";
else return "No window has focus!";
},
stageA.focusedProperty(),
stageB.focusedProperty());
labelA.textProperty().bind(text);
labelB.textProperty().bind(text);
// 'shortcut+a' focuses 'stageA', 'shortcut+b' focuses 'stageB'
EventHandler<KeyEvent> handler = e -> {
if (e.isShortcutDown()) {
if (e.getCode() == KeyCode.A && !stageA.isFocused()) {
e.consume();
stageA.requestFocus();
} else if (e.getCode() == KeyCode.B && !stageB.isFocused()) {
e.consume();
stageB.requestFocus();
}
}
};
// add key-pressed handler to all involved windows
stageA.addEventFilter(KeyEvent.KEY_PRESSED, handler);
stageB.addEventFilter(KeyEvent.KEY_PRESSED, handler);
stageA.show();
stageB.show();
// place windows side-by-side
stageA.setX(stageA.getX() - (stageA.getWidth() / 2) + 5);
stageB.setX(stageB.getX() + (stageB.getWidth() / 2) + 5);
}
}
However, if you want to be able to focus a window based on key presses when none of your application's windows are focused, then it gets a little more complicated. You'll have to hook into the platform's key events to add some kind of global handler. One library that may help you with this is jnativehook. Or you could write your own native code for this using Java Native Interface (JNI), Java Native Access (JNA), or the Foreign Function & Memory API (FFM) (that API was added in Java 22).
As for why changing the modality causes problems, see Stage#initModality(Modality)
:
Throws:
IllegalStateException
- if this property is set after the stage has ever been made visible.
IllegalStateException
- if this stage is the primary stage.
In other words, you can't set the modality of the stage passed to Application#start(Stage)
by the JavaFX framework, and you can't change the modality of any stage after is has been shown. There's not a solution to this as far as I'm aware. You'll likely just have to accept none of your windows can be modal.
Note you can abstract this switch-focus-on-key-press behavior into its own class. For example:
FocusSwitcher.java
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javafx.beans.Observable;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.event.WeakEventHandler;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.stage.Window;
/**
* A class that manages a list of registered windows and switches their focus based on specified key
* combinations.
*/
public class FocusSwitcher {
private final ObservableList<Window> registeredWindows =
FXCollections.observableArrayList(w -> new Observable[] {w.focusedProperty()});
private ObservableList<Window> unmodifiableRegisteredWindows;
private final Map<Window, Set<KeyCombination>> winToCombos = new HashMap<>();
private final Map<KeyCombination, Window> comboToWin = new HashMap<>();
private final EventHandler<KeyEvent> keyHandler = this::processKeyEvent;
private final WeakEventHandler<KeyEvent> weakKeyHandler = new WeakEventHandler<>(keyHandler);
public FocusSwitcher() {
registeredWindows.addListener(this::onRegisteredWindowsChanged);
}
/**
* Register the given window and associate it with the given key combinations. If the window is
* already registered, then the given key combinations will be added to the already associated key
* combinations. Duplicate key combinations are ignored.
*
* @param window the window to register
* @param combos the key combinations to associate with {@code window}
* @throws IllegalStateException if any key combination in {@code combos} is already associated
* with a different window
* @throws NullPointerException if {@code window}, {@code combos}, or any element of
* {@code combos} is {@code null}
*/
public void register(Window window, KeyCombination... combos) {
Objects.requireNonNull(window, "window");
Objects.requireNonNull(combos, "combos");
register(window, Arrays.asList(combos));
}
/**
* Register the given window and associate it with the given key combinations. If the window is
* already registered, then the given key combinations will be added to the already associated key
* combinations. Duplicate key combinations are ignored.
*
* @param window the window to register
* @param combos the key combinations to associate with {@code window}
* @throws IllegalStateException if any key combination in {@code combos} is already associated
* with a different window
* @throws NullPointerException if {@code window}, {@code combos}, or any element of
* {@code combos} is {@code null}
*/
public void register(Window window, Iterable<? extends KeyCombination> combos) {
Objects.requireNonNull(window, "window");
Objects.requireNonNull(combos, "combos");
for (var combo : combos) {
Objects.requireNonNull(combo, "'combos' contains null elements");
Window w;
if ((w = comboToWin.get(combo)) != null && w != window) {
throw new IllegalStateException(
"key combination already associated with different window: " + combo);
}
comboToWin.put(combo, window);
}
var comboSet = winToCombos.get(window);
if (comboSet == null) {
comboSet = new HashSet<>();
winToCombos.put(window, comboSet);
registeredWindows.add(window);
}
if (combos instanceof Collection<? extends KeyCombination> col) {
comboSet.addAll(col);
} else {
combos.forEach(comboSet::add);
}
}
/**
* Unregister the given window.
*
* @param window the window to unregister
*/
public void unregister(Window window) {
Objects.requireNonNull(window);
var comboSet = winToCombos.remove(window);
if (comboSet != null) {
comboSet.forEach(comboToWin::remove);
registeredWindows.remove(window);
}
}
private void processKeyEvent(KeyEvent event) {
var eventType = event.getEventType();
if (eventType == KeyEvent.KEY_PRESSED || eventType == KeyEvent.KEY_TYPED) {
comboToWin.entrySet().stream()
.filter(entry -> entry.getKey().match(event))
.findFirst()
.map(Map.Entry::getValue)
.filter(not(Window::isFocused))
.ifPresent(window -> {
event.consume();
window.requestFocus();
});
}
}
/**
* Returns the observable list of windows registered with this "focus switcher". The returned list
* will be unmodifiable.
*
* @return the unmodifiable observable list of registered windows
*/
public ObservableList<Window> getRegisteredWindows() {
if (unmodifiableRegisteredWindows == null) {
unmodifiableRegisteredWindows = FXCollections.unmodifiableObservableList(registeredWindows);
}
return unmodifiableRegisteredWindows;
}
private void onRegisteredWindowsChanged(ListChangeListener.Change<? extends Window> c) {
while (c.next()) {
if (c.wasUpdated()) {
var focused = getFocusedWindow();
for (var updated : c.getList().subList(c.getFrom(), c.getTo())) {
if (updated.isFocused()) {
focused = updated;
break;
}
}
if (focused != null && focused.isFocused()) {
setFocusedWindow(focused);
} else {
setFocusedWindow(null);
}
} else if (c.wasRemoved()) {
for (var removed : c.getRemoved()) {
removed.removeEventFilter(KeyEvent.ANY, weakKeyHandler);
if (getFocusedWindow() == removed) {
setFocusedWindow(null);
}
}
} else if (c.wasAdded()) {
for (var added : c.getAddedSubList()) {
added.addEventFilter(KeyEvent.ANY, weakKeyHandler);
if (added.isFocused()) {
setFocusedWindow(added);
}
}
}
}
}
/* **************************************************************************
* *
* Properties *
* *
****************************************************************************/
// -- focusedWindow property
/**
* The currently focused window from the registered windows. If none of the registered windows are
* focused then this property will contain {@code null}.
*/
private final ReadOnlyObjectWrapper<Window> focusedWindow =
new ReadOnlyObjectWrapper<>(this, "focusedWindow");
private void setFocusedWindow(Window focusedWindow) {
this.focusedWindow.set(focusedWindow);
}
public final Window getFocusedWindow() {
return focusedWindow.get();
}
public final ReadOnlyObjectProperty<Window> focusedWindowProperty() {
return focusedWindow.getReadOnlyProperty();
}
}
Main.java
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
private final FocusSwitcher switcher = new FocusSwitcher();
@Override
public void start(Stage stageA) {
var labelA = new Label();
stageA.setScene(new Scene(new StackPane(labelA), 300, 150));
stageA.setTitle("Window A");
var labelB = new Label();
var stageB = new Stage();
stageB.setScene(new Scene(new StackPane(labelB), 300, 150));
stageB.setTitle("Window B");
switcher.register(stageA, KeyCombination.valueOf("shortcut+a"));
switcher.register(stageB, KeyCombination.valueOf("shortcut+b"));
switcher.focusedWindowProperty().subscribe(win -> {
String text;
if (win == null) {
text = "No window has focus!";
} else if (win instanceof Stage s) {
text = "\"" + s.getTitle() + "\" has focus!";
} else {
text = "\"" + win + "\" has focus!";
}
labelA.setText(text);
labelB.setText(text);
});
stageA.show();
stageB.show();
// place windows side-by-side
stageA.setX(stageA.getX() - (stageA.getWidth() / 2) + 5);
stageB.setX(stageB.getX() + (stageB.getWidth() / 2) + 5);
}
}
As an aside, you seem to be having some trouble with concurrency. Note that you must not modify a live scene graph from any thread other than the JavaFX Application Thread. You seem to know that since you're wrapping code in Platform::runlater
, but your setup makes the use of threads pointless. All they do is schedule work with the JavaFX Application Thread. Plus, the work you're doing in runTask
doesn't seem to be expensive, so using a background thread is not needed in the first place.
If you need a game loop, consider using an AnimationTimer
or a Timeline
. See JavaFX periodic background task for more information.
Platform.runLater
? Also, what's the purpose of having two classes that extendApplication
? – SedJ601 Commented Mar 14 at 14:30