improved menu accessibility

main
Jonah Bauer 3 years ago
parent 2375a5ed83
commit 3cb756a324

@ -17,6 +17,8 @@ import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.utils.I18NBundle; import com.badlogic.gdx.utils.I18NBundle;
import com.badlogic.gdx.utils.viewport.ExtendViewport; import com.badlogic.gdx.utils.viewport.ExtendViewport;
import com.badlogic.gdx.utils.viewport.FitViewport; import com.badlogic.gdx.utils.viewport.FitViewport;
import eu.jonahbauer.wizard.client.libgdx.listeners.AutoFocusListener;
import eu.jonahbauer.wizard.client.libgdx.listeners.ButtonKeyListener;
import eu.jonahbauer.wizard.client.libgdx.screens.MainMenuScreen; import eu.jonahbauer.wizard.client.libgdx.screens.MainMenuScreen;
import lombok.Getter; import lombok.Getter;
@ -137,10 +139,13 @@ public class WizardGame extends Game {
if (WizardGame.DEBUG) { if (WizardGame.DEBUG) {
stage.setDebugAll(true); stage.setDebugAll(true);
} }
reset();
} }
public void reset() { public void reset() {
stage.clear(); stage.clear();
stage.addListener(new ButtonKeyListener());
stage.addListener(new AutoFocusListener());
} }
public void dispose() { public void dispose() {

@ -1,7 +1,7 @@
package eu.jonahbauer.wizard.client.libgdx.actions.overlay; package eu.jonahbauer.wizard.client.libgdx.actions.overlay;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup; import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import eu.jonahbauer.wizard.client.libgdx.actors.game.CardActor; import eu.jonahbauer.wizard.client.libgdx.actors.CardActor;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.common.model.Card; import eu.jonahbauer.wizard.common.model.Card;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;

@ -6,7 +6,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.HorizontalGroup;
import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup; import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import eu.jonahbauer.wizard.client.libgdx.actors.game.CardActor; import eu.jonahbauer.wizard.client.libgdx.actors.CardActor;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.common.model.Card; import eu.jonahbauer.wizard.common.model.Card;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;

@ -8,7 +8,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup; import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import eu.jonahbauer.wizard.client.libgdx.actors.game.CardActor; import eu.jonahbauer.wizard.client.libgdx.actors.CardActor;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.common.model.Card; import eu.jonahbauer.wizard.common.model.Card;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;

@ -9,7 +9,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup; import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import eu.jonahbauer.wizard.client.libgdx.AnimationTimings; import eu.jonahbauer.wizard.client.libgdx.AnimationTimings;
import eu.jonahbauer.wizard.client.libgdx.actions.MyActions; import eu.jonahbauer.wizard.client.libgdx.actions.MyActions;
import eu.jonahbauer.wizard.client.libgdx.actors.game.CardActor; import eu.jonahbauer.wizard.client.libgdx.actors.CardActor;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.common.model.Card; import eu.jonahbauer.wizard.common.model.Card;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;

@ -1,45 +0,0 @@
package eu.jonahbauer.wizard.client.libgdx.actors;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
@SuppressWarnings("unused")
public class AutoFocusScrollPane extends ScrollPane {
public AutoFocusScrollPane(Actor widget) {
super(widget);
init();
}
public AutoFocusScrollPane(Actor widget, Skin skin) {
super(widget, skin);
init();
}
public AutoFocusScrollPane(Actor widget, Skin skin, String styleName) {
super(widget, skin, styleName);
init();
}
public AutoFocusScrollPane(Actor widget, ScrollPaneStyle style) {
super(widget, style);
init();
}
private void init() {
addListener(new InputListener() {
public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) {
var stage = getStage();
if (stage != null) stage.setScrollFocus(AutoFocusScrollPane.this);
}
public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) {
var stage = getStage();
if (stage != null) stage.setScrollFocus(null);
}
});
}
}

@ -1,4 +1,4 @@
package eu.jonahbauer.wizard.client.libgdx.actors.game; package eu.jonahbauer.wizard.client.libgdx.actors;
import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.graphics.g2d.TextureAtlas;
@ -112,7 +112,6 @@ public class CardActor extends Actor {
this.border = atlas.findRegion(GameAtlas.CARDS_BORDER2); this.border = atlas.findRegion(GameAtlas.CARDS_BORDER2);
setWidth(PREF_WIDTH); setWidth(PREF_WIDTH);
setHeight(PREF_HEIGHT); setHeight(PREF_HEIGHT);
setColor(0.8f, 0.8f, 0.8f, 1.0f);
setOrigin(PREF_WIDTH / 2, PREF_HEIGHT / 2); setOrigin(PREF_WIDTH / 2, PREF_HEIGHT / 2);
} }

@ -1,4 +1,4 @@
package eu.jonahbauer.wizard.client.libgdx.actors.game; package eu.jonahbauer.wizard.client.libgdx.actors;
import com.badlogic.gdx.scenes.scene2d.*; import com.badlogic.gdx.scenes.scene2d.*;
import eu.jonahbauer.wizard.client.libgdx.AnimationTimings; import eu.jonahbauer.wizard.client.libgdx.AnimationTimings;

@ -1,4 +1,4 @@
package eu.jonahbauer.wizard.client.libgdx.actors.game; package eu.jonahbauer.wizard.client.libgdx.actors;
import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.MathUtils;

@ -1,4 +1,4 @@
package eu.jonahbauer.wizard.client.libgdx.actors.game; package eu.jonahbauer.wizard.client.libgdx.actors;
import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Actor;
@ -14,6 +14,7 @@ import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.Align;
import eu.jonahbauer.wizard.client.libgdx.AnimationTimings; import eu.jonahbauer.wizard.client.libgdx.AnimationTimings;
import lombok.Setter; import lombok.Setter;
import org.jetbrains.annotations.Range;
public class PadOfTruth extends Table { public class PadOfTruth extends Table {
public static final float EXTENDED_WIDTH = 636; public static final float EXTENDED_WIDTH = 636;
@ -96,11 +97,16 @@ public class PadOfTruth extends Table {
names[player].setText(name); names[player].setText(name);
} }
public void setScore(int player, int round, int score) { public void setScore(int player, @Range(from = 0, to = 19) int round, int score) {
scores[round][player].setText(String.valueOf(score)); scores[round][player].setText(String.valueOf(score));
} }
public void setPrediction(int player, int round, int prediction) { public void setPrediction(int player, @Range(from = 0, to = 19) int round, int prediction) {
predictions[round][player].setText(String.valueOf(prediction)); predictions[round][player].setText(String.valueOf(prediction));
} }
public void checkPosition(int player, int round) {
if (round < 0 || round >= predictions.length || round >= scores.length) throw new ArrayIndexOutOfBoundsException(round);
if (player < 0 || player >= predictions[round].length || player >= scores[round].length) throw new ArrayIndexOutOfBoundsException(player);
}
} }

@ -0,0 +1,36 @@
package eu.jonahbauer.wizard.client.libgdx.listeners;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
public class AutoFocusListener extends InputListener {
@Override
public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) {
var target = event.getTarget();
while (target != null && !(target instanceof ScrollPane)) {
target = target.getParent();
}
if (target instanceof ScrollPane pane) {
event.getStage().setScrollFocus(pane);
} else {
event.getStage().setScrollFocus(null);
}
}
@Override
public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) {
var target = event.getTarget();
while (target != null && !(target instanceof ScrollPane)) {
target = target.getParent();
}
if (target == null || toActor == null || !toActor.isDescendantOf(target)) {
event.getStage().setScrollFocus(null);
}
}
}

@ -0,0 +1,17 @@
package eu.jonahbauer.wizard.client.libgdx.listeners;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.ui.Button;
public class ButtonKeyListener extends InputListener {
@Override
public boolean keyTyped(InputEvent event, char character) {
if ((character == '\n' || character == ' ') && event.getTarget() instanceof Button button) {
button.toggle();
return true;
}
return false;
}
}

@ -0,0 +1,48 @@
package eu.jonahbauer.wizard.client.libgdx.listeners;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.TextField;
import com.badlogic.gdx.scenes.scene2d.utils.UIUtils;
import java.util.List;
public class KeyboardFocusManager extends InputListener {
private final Stage stage;
private final List<Actor> focusOrder;
public KeyboardFocusManager(Stage stage, List<Actor> focusOrder) {
this.stage = stage;
this.focusOrder = focusOrder;
}
@Override
public boolean keyTyped(InputEvent event, char character) {
if (character == '\t') {
var currentFocus = stage.getKeyboardFocus();
var index = currentFocus == null ? -1 : focusOrder.indexOf(currentFocus);
var count = focusOrder.size();
if (count == 0) return true;
Actor nextFocus;
if (index == -1) {
nextFocus = focusOrder.get(UIUtils.shift() ? count - 1 : 0);
} else {
var direction = UIUtils.shift() ? -1 : 1;
nextFocus = focusOrder.get(((index + direction) % count + count) % count);
}
if (nextFocus instanceof TextField textField) {
textField.selectAll();
}
stage.setKeyboardFocus(nextFocus);
event.stop();
return true;
} else {
return false;
}
}
}

@ -0,0 +1,44 @@
package eu.jonahbauer.wizard.client.libgdx.listeners;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.ui.SelectBox;
public class SelectBoxListener extends InputListener {
private final SelectBox<?> selectBox;
public SelectBoxListener(SelectBox<?> selectBox) {
this.selectBox = selectBox;
}
@Override
public boolean keyDown(InputEvent event, int keycode) {
var size = selectBox.getItems().size;
if (size == 0) return false;
switch (keycode) {
case Input.Keys.UP -> {
var index = selectBox.getSelectedIndex();
if (index == -1) {
selectBox.setSelectedIndex(size - 1);
} else {
selectBox.setSelectedIndex(MathUtils.clamp(index - 1, 0, size - 1));
}
return true;
}
case Input.Keys.DOWN -> {
var index = selectBox.getSelectedIndex();
if (index == -1) {
selectBox.setSelectedIndex(0);
} else {
selectBox.setSelectedIndex(MathUtils.clamp(index + 1, 0, size - 1));
}
return true;
}
}
return false;
}
}

@ -8,18 +8,21 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextField;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.Align;
import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.listeners.KeyboardFocusManager;
import eu.jonahbauer.wizard.client.libgdx.listeners.ResetErrorListener; import eu.jonahbauer.wizard.client.libgdx.listeners.ResetErrorListener;
import eu.jonahbauer.wizard.client.libgdx.state.Menu; import eu.jonahbauer.wizard.client.libgdx.state.Menu;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.List;
public class ConnectScreen extends MenuScreen { public class ConnectScreen extends MenuScreen {
private static String uri;
private TextButton buttonBack; private TextButton buttonBack;
private TextButton buttonConnect; private TextButton buttonConnect;
private TextField uri; private TextField uriField;
public ConnectScreen(WizardGame game) { public ConnectScreen(WizardGame game) {
super(game); super(game);
@ -33,10 +36,12 @@ public class ConnectScreen extends MenuScreen {
sfxClick(); sfxClick();
} else if (actor == buttonConnect) { } else if (actor == buttonConnect) {
try { try {
var uri = new URI(ConnectScreen.this.uri.getText()); var uriString = ConnectScreen.this.uriField.getText();
var uri = new URI(uriString);
ConnectScreen.uri = uriString;
game.getClient().execute(Menu.class, (s, c) -> s.connect(c, uri)); game.getClient().execute(Menu.class, (s, c) -> s.connect(c, uri));
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
uri.setStyle(game.data.skin.get("error", TextField.TextFieldStyle.class)); uriField.setStyle(game.data.skin.get("error", TextField.TextFieldStyle.class));
} }
sfxClick(); sfxClick();
@ -50,8 +55,11 @@ public class ConnectScreen extends MenuScreen {
buttonBack = new TextButton(game.messages.get("menu.connect.back"), game.data.skin); buttonBack = new TextButton(game.messages.get("menu.connect.back"), game.data.skin);
buttonBack.setPosition(WizardGame.WIDTH * 0.275f, BUTTON_BAR_Y); buttonBack.setPosition(WizardGame.WIDTH * 0.275f, BUTTON_BAR_Y);
buttonBack.addListener(listener);
buttonConnect = new TextButton(game.messages.get("menu.connect.connect"), game.data.skin); buttonConnect = new TextButton(game.messages.get("menu.connect.connect"), game.data.skin);
buttonConnect.setPosition(WizardGame.WIDTH * 0.725f - buttonConnect.getWidth(), BUTTON_BAR_Y); buttonConnect.setPosition(WizardGame.WIDTH * 0.725f - buttonConnect.getWidth(), BUTTON_BAR_Y);
buttonConnect.addListener(listener);
var label = new Label(game.messages.get("menu.connect.address.label"), game.data.skin); var label = new Label(game.messages.get("menu.connect.address.label"), game.data.skin);
label.setSize(0.4f * WizardGame.WIDTH, 64); label.setSize(0.4f * WizardGame.WIDTH, 64);
@ -59,19 +67,24 @@ public class ConnectScreen extends MenuScreen {
label.setPosition(0.5f * (WizardGame.WIDTH - label.getWidth()), 0.55f * (WizardGame.HEIGHT - label.getHeight())); label.setPosition(0.5f * (WizardGame.WIDTH - label.getWidth()), 0.55f * (WizardGame.HEIGHT - label.getHeight()));
// TODO sensible default value // TODO sensible default value
uri = new TextField("wss://webdev.jonahbauer.eu/wizard/", game.data.skin); uriField = new TextField(uri != null ? uri : "wss://webdev.jonahbauer.eu/wizard/", game.data.skin);
uri.setMessageText(game.messages.get("menu.connect.uri.hint")); uriField.setMessageText(game.messages.get("menu.connect.uri.hint"));
uri.setSize(0.4f * WizardGame.WIDTH, 64); uriField.setSize(0.4f * WizardGame.WIDTH, 64);
uri.setPosition(0.5f * (WizardGame.WIDTH - uri.getWidth()), 0.45f * (WizardGame.HEIGHT - uri.getHeight())); uriField.setPosition(0.5f * (WizardGame.WIDTH - uriField.getWidth()), 0.45f * (WizardGame.HEIGHT - uriField.getHeight()));
uri.addListener(new ResetErrorListener(game.data.skin)); uriField.addListener(new ResetErrorListener(game.data.skin));
Gdx.input.setInputProcessor(game.data.stage); Gdx.input.setInputProcessor(game.data.stage);
game.data.stage.addActor(buttonBack); game.data.stage.addActor(buttonBack);
game.data.stage.addActor(buttonConnect); game.data.stage.addActor(buttonConnect);
game.data.stage.addActor(uri); game.data.stage.addActor(uriField);
game.data.stage.addActor(label); game.data.stage.addActor(label);
game.data.stage.addCaptureListener(new KeyboardFocusManager(
game.data.stage,
List.of(uriField, buttonBack, buttonConnect)
));
buttonBack.addListener(listener); buttonBack.setName("button_back");
buttonConnect.addListener(listener); buttonConnect.setName("button_connect");
uriField.setName("uri");
} }
} }

@ -6,7 +6,9 @@ import com.badlogic.gdx.scenes.scene2d.ui.*;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Array;
import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.listeners.KeyboardFocusManager;
import eu.jonahbauer.wizard.client.libgdx.listeners.ResetErrorListener; import eu.jonahbauer.wizard.client.libgdx.listeners.ResetErrorListener;
import eu.jonahbauer.wizard.client.libgdx.listeners.SelectBoxListener;
import eu.jonahbauer.wizard.client.libgdx.state.Lobby; import eu.jonahbauer.wizard.client.libgdx.state.Lobby;
import eu.jonahbauer.wizard.common.model.Configuration; import eu.jonahbauer.wizard.common.model.Configuration;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
@ -28,10 +30,7 @@ public class CreateGameScreen extends MenuScreen {
@Override @Override
public void changed(ChangeEvent event, Actor actor) { public void changed(ChangeEvent event, Actor actor) {
if (actor == buttonBack) { if (actor == buttonBack) {
game.getClient().execute(Lobby.class, (s, c) -> { game.getClient().execute(Lobby.class, Lobby::showListScreen);
s.setPlayerName(playerName.getText());
return s.showListScreen(c);
});
sfxClick(); sfxClick();
} else if (actor == buttonContinue) { } else if (actor == buttonContinue) {
create(); create();
@ -51,8 +50,11 @@ public class CreateGameScreen extends MenuScreen {
buttonBack = new TextButton(game.messages.get("menu.create_game.back"), game.data.skin); buttonBack = new TextButton(game.messages.get("menu.create_game.back"), game.data.skin);
buttonBack.setPosition(WizardGame.WIDTH * 0.275f, BUTTON_BAR_Y); buttonBack.setPosition(WizardGame.WIDTH * 0.275f, BUTTON_BAR_Y);
buttonBack.addListener(listener);
buttonContinue = new TextButton(game.messages.get("menu.create_game.create"), game.data.skin); buttonContinue = new TextButton(game.messages.get("menu.create_game.create"), game.data.skin);
buttonContinue.setPosition(WizardGame.WIDTH * 0.725f - buttonContinue.getWidth(), BUTTON_BAR_Y); buttonContinue.setPosition(WizardGame.WIDTH * 0.725f - buttonContinue.getWidth(), BUTTON_BAR_Y);
buttonContinue.addListener(listener);
var errorListener = new ResetErrorListener(game.data.skin); var errorListener = new ResetErrorListener(game.data.skin);
@ -73,6 +75,7 @@ public class CreateGameScreen extends MenuScreen {
public void changed(ChangeEvent event, Actor actor) { public void changed(ChangeEvent event, Actor actor) {
var player = playerName.getText(); var player = playerName.getText();
var session = sessionName.getText(); var session = sessionName.getText();
Lobby.setPlayerName(player);
if (session.isEmpty() || session.equals(format.formatted(oldPlayerName))) { if (session.isEmpty() || session.equals(format.formatted(oldPlayerName))) {
if (player.isEmpty()) { if (player.isEmpty()) {
sessionName.setText(""); sessionName.setText("");
@ -97,6 +100,7 @@ public class CreateGameScreen extends MenuScreen {
configurations.setSize(400, 64); configurations.setSize(400, 64);
configurations.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.3f); configurations.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.3f);
configurations.addListener(errorListener); configurations.addListener(errorListener);
configurations.addListener(new SelectBoxListener(configurations));
Array<String> values = new Array<>(); Array<String> values = new Array<>();
for (Configuration value : Configuration.values()) { for (Configuration value : Configuration.values()) {
@ -122,9 +126,17 @@ public class CreateGameScreen extends MenuScreen {
game.data.stage.addActor(buttonContinue); game.data.stage.addActor(buttonContinue);
game.data.stage.addActor(contentTable); game.data.stage.addActor(contentTable);
game.data.stage.addActor(buttonBack); game.data.stage.addActor(buttonBack);
game.data.stage.addCaptureListener(new KeyboardFocusManager(
buttonContinue.addListener(listener); game.data.stage,
buttonBack.addListener(listener); java.util.List.of(playerName, sessionName, timeOut, configurations, buttonBack, buttonContinue)
));
buttonBack.setName("button_back");
buttonContinue.setName("button_continue");
sessionName.setName("session_name");
playerName.setName("player_name");
timeOut.setName("timeout");
configurations.setName("configurations");
} }
private void create() { private void create() {

@ -17,10 +17,10 @@ import eu.jonahbauer.wizard.client.libgdx.AnimationTimings;
import eu.jonahbauer.wizard.client.libgdx.GameAtlas; import eu.jonahbauer.wizard.client.libgdx.GameAtlas;
import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.actions.overlay.*; import eu.jonahbauer.wizard.client.libgdx.actions.overlay.*;
import eu.jonahbauer.wizard.client.libgdx.actors.game.CardActor; import eu.jonahbauer.wizard.client.libgdx.actors.CardActor;
import eu.jonahbauer.wizard.client.libgdx.actors.game.CardStack; import eu.jonahbauer.wizard.client.libgdx.actors.CardStack;
import eu.jonahbauer.wizard.client.libgdx.actors.game.CardsGroup; import eu.jonahbauer.wizard.client.libgdx.actors.CardsGroup;
import eu.jonahbauer.wizard.client.libgdx.actors.game.PadOfTruth; import eu.jonahbauer.wizard.client.libgdx.actors.PadOfTruth;
import eu.jonahbauer.wizard.client.libgdx.state.Game; import eu.jonahbauer.wizard.client.libgdx.state.Game;
import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.common.model.Card; import eu.jonahbauer.wizard.common.model.Card;
@ -28,6 +28,7 @@ import lombok.Getter;
import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -77,7 +78,6 @@ public class GameScreen extends MenuScreen {
public void show() { public void show() {
super.show(); super.show();
atlas = new TextureAtlas(Gdx.files.internal(GameAtlas.$PATH)); atlas = new TextureAtlas(Gdx.files.internal(GameAtlas.$PATH));
// overlay = new TextureRegionDrawable(game.data.uiskinAtlas.findRegion(UiskinAtlas.WHITE)).tint(new Color(0,0,0,0.5f));
labelStyleDefault = game.data.skin.get(Label.LabelStyle.class); labelStyleDefault = game.data.skin.get(Label.LabelStyle.class);
labelStyleActive = new Label.LabelStyle(labelStyleDefault); labelStyleActive = new Label.LabelStyle(labelStyleDefault);
labelStyleActive.fontColor = Color.RED; labelStyleActive.fontColor = Color.RED;
@ -291,7 +291,7 @@ public class GameScreen extends MenuScreen {
* Adds the prediction for the given round and player to the {@linkplain #padOfTruth pad of truth} and * Adds the prediction for the given round and player to the {@linkplain #padOfTruth pad of truth} and
* shows a corresponding message. * shows a corresponding message.
*/ */
public void addPrediction(int round, @NotNull UUID player, int prediction, boolean changed) { public void addPrediction(@Range(from = 0, to = 19) int round, @NotNull UUID player, int prediction, boolean changed) {
if (isSelf(player)) { if (isSelf(player)) {
addMessage(game.messages.format("game.action." + (changed ? "change" : "make") + "_prediction.self", prediction)); addMessage(game.messages.format("game.action." + (changed ? "change" : "make") + "_prediction.self", prediction));
} else { } else {
@ -302,6 +302,8 @@ public class GameScreen extends MenuScreen {
var index = orderedPlayers.indexOf(player); var index = orderedPlayers.indexOf(player);
if (index == -1) throw new NoSuchElementException(); if (index == -1) throw new NoSuchElementException();
padOfTruth.checkPosition(index, round);
execute(() -> padOfTruth.setPrediction(index, round, prediction)); execute(() -> padOfTruth.setPrediction(index, round, prediction));
} }
@ -339,7 +341,8 @@ public class GameScreen extends MenuScreen {
/** /**
* Adds the scores for a round to the corresponding row of the {@linkplain #padOfTruth pad of truth}. * Adds the scores for a round to the corresponding row of the {@linkplain #padOfTruth pad of truth}.
*/ */
public void addScores(int round, @NotNull Map<UUID, Integer> scores) { public void addScores(@Range(from = 0, to = 19) int round, @NotNull Map<UUID, Integer> scores) {
padOfTruth.checkPosition(orderedPlayers.size(), round);
execute(() -> { execute(() -> {
for (int i = 0; i < orderedPlayers.size(); i++) { for (int i = 0; i < orderedPlayers.size(); i++) {
UUID player = orderedPlayers.get(i); UUID player = orderedPlayers.get(i);

@ -5,7 +5,7 @@ import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.*; import com.badlogic.gdx.scenes.scene2d.ui.*;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.actors.AutoFocusScrollPane; import eu.jonahbauer.wizard.client.libgdx.listeners.KeyboardFocusManager;
import eu.jonahbauer.wizard.client.libgdx.listeners.ResetErrorListener; import eu.jonahbauer.wizard.client.libgdx.listeners.ResetErrorListener;
import eu.jonahbauer.wizard.client.libgdx.state.Lobby; import eu.jonahbauer.wizard.client.libgdx.state.Lobby;
import eu.jonahbauer.wizard.common.messages.data.SessionData; import eu.jonahbauer.wizard.common.messages.data.SessionData;
@ -41,10 +41,7 @@ public class LobbyScreen extends MenuScreen {
join(); join();
sfxClick(); sfxClick();
} else if (actor == buttonCreate) { } else if (actor == buttonCreate) {
game.getClient().execute(Lobby.class, (s, c) -> { game.getClient().execute(Lobby.class, Lobby::showCreateScreen);
s.setPlayerName(playerName.getText());
return s.showCreateScreen(c);
});
sfxClick(); sfxClick();
} }
} }
@ -60,8 +57,13 @@ public class LobbyScreen extends MenuScreen {
super.show(); super.show();
buttonBack = new TextButton(game.messages.get("menu.lobby.back"), game.data.skin); buttonBack = new TextButton(game.messages.get("menu.lobby.back"), game.data.skin);
buttonBack.addListener(listener);
buttonJoin = new TextButton(game.messages.get("menu.lobby.join"), game.data.skin); buttonJoin = new TextButton(game.messages.get("menu.lobby.join"), game.data.skin);
buttonJoin.addListener(listener);
buttonCreate = new TextButton(game.messages.get("menu.lobby.create"), game.data.skin); buttonCreate = new TextButton(game.messages.get("menu.lobby.create"), game.data.skin);
buttonCreate.addListener(listener);
sessions = new List<>(game.data.skin) { sessions = new List<>(game.data.skin) {
@Override @Override
@ -77,7 +79,7 @@ public class LobbyScreen extends MenuScreen {
} }
}); });
sessionListContainer = new AutoFocusScrollPane(sessions, game.data.skin); sessionListContainer = new ScrollPane(sessions, game.data.skin);
sessionListContainer.layout(); sessionListContainer.layout();
sessions.addListener(new ResetErrorListener(game.data.skin, sessionListContainer)); sessions.addListener(new ResetErrorListener(game.data.skin, sessionListContainer));
@ -100,10 +102,19 @@ public class LobbyScreen extends MenuScreen {
Gdx.input.setInputProcessor(game.data.stage); Gdx.input.setInputProcessor(game.data.stage);
game.data.stage.addActor(content); game.data.stage.addActor(content);
game.data.stage.addActor(buttons); game.data.stage.addActor(buttons);
game.data.stage.addCaptureListener(new KeyboardFocusManager(
game.data.stage,
java.util.List.of(sessions, playerName, buttonBack, buttonCreate, buttonJoin)
));
buttonBack.addListener(listener); buttonBack.setName("button_back");
buttonJoin.addListener(listener); buttonJoin.setName("button_join");
buttonCreate.addListener(listener); buttonCreate.setName("button_create");
sessions.setName("session_list");
playerName.setName("player_name");
labelSessionName.setName("session_name");
labelSessionConfiguration.setName("session_configuration");
labelSessionPlayerCount.setName("session_player_count");
} }
public void addSession(SessionData session) { public void addSession(SessionData session) {
@ -112,14 +123,11 @@ public class LobbyScreen extends MenuScreen {
} }
public void removeSession(UUID session) { public void removeSession(UUID session) {
var items = this.sessions.getItems(); var index = indexOf(session);
for (int i = 0; i < items.size; i++) { if (index != -1) {
if (items.get(i).getUuid().equals(session)) { this.sessions.getItems().removeIndex(index);
items.removeIndex(i);
break;
}
}
this.sessions.invalidateHierarchy(); this.sessions.invalidateHierarchy();
}
if (selectedSession != null && selectedSession.getUuid().equals(session)) { if (selectedSession != null && selectedSession.getUuid().equals(session)) {
updateData(null); updateData(null);
@ -127,13 +135,8 @@ public class LobbyScreen extends MenuScreen {
} }
public void modifySession(SessionData session) { public void modifySession(SessionData session) {
var items = this.sessions.getItems(); var index = indexOf(session.getUuid());
for (int i = 0; i < items.size; i++) { this.sessions.getItems().set(index, session);
if (items.get(i).getUuid().equals(session.getUuid())) {
items.set(i, session);
break;
}
}
this.sessions.invalidateHierarchy(); this.sessions.invalidateHierarchy();
if (selectedSession != null && selectedSession.getUuid().equals(session.getUuid())) { if (selectedSession != null && selectedSession.getUuid().equals(session.getUuid())) {
@ -167,6 +170,12 @@ public class LobbyScreen extends MenuScreen {
float infoTableWidth = 0.3f * WizardGame.WIDTH - 20; float infoTableWidth = 0.3f * WizardGame.WIDTH - 20;
playerName = new TextField(oldPlayerName, game.data.skin); playerName = new TextField(oldPlayerName, game.data.skin);
playerName.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
Lobby.setPlayerName(playerName.getText());
}
});
playerName.addListener(new ResetErrorListener(game.data.skin)); playerName.addListener(new ResetErrorListener(game.data.skin));
labelSessionName = new Label("", game.data.skin, "textfield"); labelSessionName = new Label("", game.data.skin, "textfield");
@ -214,4 +223,14 @@ public class LobbyScreen extends MenuScreen {
client.execute(Lobby.class, (s, c) -> s.joinSession(client, selectedSession, playerName)); client.execute(Lobby.class, (s, c) -> s.joinSession(client, selectedSession, playerName));
} }
} }
private int indexOf(UUID session) {
var items = this.sessions.getItems();
for (int i = 0; i < items.size; i++) {
if (items.get(i).getUuid().equals(session)) {
return i;
}
}
return -1;
}
} }

@ -5,8 +5,11 @@ import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton; import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.listeners.KeyboardFocusManager;
import eu.jonahbauer.wizard.client.libgdx.state.Menu; import eu.jonahbauer.wizard.client.libgdx.state.Menu;
import java.util.List;
public class MainMenuScreen extends MenuScreen { public class MainMenuScreen extends MenuScreen {
private TextButton buttonPlay; private TextButton buttonPlay;
@ -35,15 +38,22 @@ public class MainMenuScreen extends MenuScreen {
buttonPlay = new TextButton(game.messages.get("menu.main.play"), game.data.skin); buttonPlay = new TextButton(game.messages.get("menu.main.play"), game.data.skin);
buttonPlay.setPosition((WizardGame.WIDTH - buttonPlay.getWidth()) / 2f, 192 + 504 - 120f - 125f); buttonPlay.setPosition((WizardGame.WIDTH - buttonPlay.getWidth()) / 2f, 192 + 504 - 120f - 125f);
buttonPlay.addListener(listener);
buttonQuit = new TextButton(game.messages.get("menu.main.quit"), game.data.skin); buttonQuit = new TextButton(game.messages.get("menu.main.quit"), game.data.skin);
buttonQuit.setPosition((WizardGame.WIDTH - buttonQuit.getWidth()) / 2f, 192 + 120f); buttonQuit.setPosition((WizardGame.WIDTH - buttonQuit.getWidth()) / 2f, 192 + 120f);
buttonQuit.addListener(listener);
Gdx.input.setInputProcessor(game.data.stage); Gdx.input.setInputProcessor(game.data.stage);
game.data.stage.addActor(buttonPlay); game.data.stage.addActor(buttonPlay);
game.data.stage.addActor(buttonQuit); game.data.stage.addActor(buttonQuit);
game.data.stage.addCaptureListener(new KeyboardFocusManager(
game.data.stage,
List.of(buttonPlay, buttonQuit)
));
buttonPlay.addListener(listener); buttonPlay.setName("button_player");
buttonQuit.addListener(listener); buttonQuit.setName("button_quit");
} }
@Override @Override

@ -11,7 +11,7 @@ import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.Align;
import eu.jonahbauer.wizard.client.libgdx.MenuAtlas; import eu.jonahbauer.wizard.client.libgdx.MenuAtlas;
import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.actors.AutoFocusScrollPane; import eu.jonahbauer.wizard.client.libgdx.listeners.KeyboardFocusManager;
import eu.jonahbauer.wizard.client.libgdx.state.Session; import eu.jonahbauer.wizard.client.libgdx.state.Session;
import eu.jonahbauer.wizard.common.messages.data.PlayerData; import eu.jonahbauer.wizard.common.messages.data.PlayerData;
import eu.jonahbauer.wizard.common.model.Configuration; import eu.jonahbauer.wizard.common.model.Configuration;
@ -53,8 +53,11 @@ public class WaitingScreen extends MenuScreen {
buttonLeave = new TextButton(game.messages.get("menu.waiting.leave"), game.data.skin); buttonLeave = new TextButton(game.messages.get("menu.waiting.leave"), game.data.skin);
buttonLeave.setPosition(WizardGame.WIDTH * 0.275f, BUTTON_BAR_Y); buttonLeave.setPosition(WizardGame.WIDTH * 0.275f, BUTTON_BAR_Y);
buttonLeave.addListener(listener);
buttonReady = new TextButton(game.messages.get("menu.waiting.ready"), game.data.skin); buttonReady = new TextButton(game.messages.get("menu.waiting.ready"), game.data.skin);
buttonReady.setPosition(WizardGame.WIDTH * 0.725f - buttonReady.getWidth(), BUTTON_BAR_Y); buttonReady.setPosition(WizardGame.WIDTH * 0.725f - buttonReady.getWidth(), BUTTON_BAR_Y);
buttonReady.addListener(listener);
players = new List<>(game.data.skin) { players = new List<>(game.data.skin) {
private final TextureRegion ready = game.data.menuAtlas.findRegion(MenuAtlas.READY); private final TextureRegion ready = game.data.menuAtlas.findRegion(MenuAtlas.READY);
@ -79,7 +82,7 @@ public class WaitingScreen extends MenuScreen {
} }
}; };
var listContainer = new AutoFocusScrollPane(players, game.data.skin); var listContainer = new ScrollPane(players, game.data.skin);
listContainer.layout(); listContainer.layout();
var content = new HorizontalGroup().grow().space(20); var content = new HorizontalGroup().grow().space(20);
@ -93,9 +96,17 @@ public class WaitingScreen extends MenuScreen {
game.data.stage.addActor(buttonLeave); game.data.stage.addActor(buttonLeave);
game.data.stage.addActor(buttonReady); game.data.stage.addActor(buttonReady);
game.data.stage.addActor(content); game.data.stage.addActor(content);
game.data.stage.addCaptureListener(new KeyboardFocusManager(
game.data.stage,
java.util.List.of(buttonLeave, buttonReady)
));
buttonLeave.addListener(listener); buttonLeave.setName("button_leave");
buttonReady.addListener(listener); buttonReady.setName("button_ready");
labelSessionName.setName("session_name");
labelSessionUUID.setName("session_uuid");
labelSessionConfiguration.setName("session_configuration");
labelPlayerName.setName("player_name");
} }
public void setSending(boolean sending) { public void setSending(boolean sending) {
@ -108,35 +119,20 @@ public class WaitingScreen extends MenuScreen {
public void addPlayer(PlayerData player) { public void addPlayer(PlayerData player) {
this.players.getItems().add(player); this.players.getItems().add(player);
var items = this.players.getItems();
for (int i = 0; i < items.size; i++) {
if (items.get(i).getUuid().equals(player.getUuid())) {
items.set(i, player);
break;
}
}
this.players.invalidateHierarchy(); this.players.invalidateHierarchy();
} }
public void removePlayer(UUID player) { public void removePlayer(UUID player) {
var items = this.players.getItems(); var index = indexOf(player);
for (int i = 0; i < items.size; i++) { if (index != -1) {
if (items.get(i).getUuid().equals(player)) { this.players.getItems().removeIndex(index);
items.removeIndex(i);
break;
}
}
this.players.invalidateHierarchy(); this.players.invalidateHierarchy();
} }
}
public void modifyPlayer(PlayerData data) { public void modifyPlayer(PlayerData data) {
var items = this.players.getItems(); var index = indexOf(data.getUuid());
for (int i = 0; i < items.size; i++) { this.players.getItems().set(index, data);
if (items.get(i).getUuid().equals(data.getUuid())) {
items.set(i, data);
break;
}
}
this.players.invalidateHierarchy(); this.players.invalidateHierarchy();
} }
@ -185,4 +181,14 @@ public class WaitingScreen extends MenuScreen {
return infoTable; return infoTable;
} }
private int indexOf(UUID player) {
var items = this.players.getItems();
for (int i = 0; i < items.size; i++) {
if (items.get(i).getUuid().equals(player)) {
return i;
}
}
return -1;
}
} }

@ -14,15 +14,14 @@ import lombok.Setter;
import java.util.*; import java.util.*;
public final class Lobby extends BaseState { public final class Lobby extends BaseState {
@Getter
@Setter
private static String playerName = "";
private final Map<UUID, SessionData> sessions = new HashMap<>(); private final Map<UUID, SessionData> sessions = new HashMap<>();
private LobbyScreen lobbyScreen; private LobbyScreen lobbyScreen;
@Getter
@Setter
private String playerName = "";
public Lobby(SessionListMessage list) { public Lobby(SessionListMessage list) {
list.getSessions().forEach(s -> sessions.put(s.getUuid(), s)); list.getSessions().forEach(s -> sessions.put(s.getUuid(), s));
} }

@ -24,7 +24,7 @@ public final class Session extends BaseState {
private final UUID session; private final UUID session;
private final String sessionName; private final String sessionName;
private final Configuration configuration; private final Configuration configuration;
private final Map<UUID, PlayerData> players = new LinkedHashMap<>(); private final LinkedHashMap<UUID, PlayerData> players = new LinkedHashMap<>();
private boolean sending; private boolean sending;
@ -32,7 +32,7 @@ public final class Session extends BaseState {
this.session = session.getUuid(); this.session = session.getUuid();
this.sessionName = session.getName(); this.sessionName = session.getName();
this.configuration = session.getConfiguration(); this.configuration = session.getConfiguration();
players.forEach(player -> this.players.put(player.getUuid(), player)); players.forEach(p -> this.players.put(p.getUuid(), p));
this.self = self; this.self = self;
this.secret = secret; this.secret = secret;
@ -54,19 +54,21 @@ public final class Session extends BaseState {
public Optional<ClientState> onMessage(Client client, ServerMessage message) { public Optional<ClientState> onMessage(Client client, ServerMessage message) {
if (message instanceof PlayerJoinedMessage join) { if (message instanceof PlayerJoinedMessage join) {
var player = join.getPlayer(); var player = join.getPlayer();
sessionScreen.addPlayer(player); log.info("Player {} joined the session.", player.getName());
players.put(player.getUuid(), player); players.put(player.getUuid(), player);
sessionScreen.addPlayer(player);
return Optional.empty(); return Optional.empty();
} else if (message instanceof PlayerLeftMessage leave) { } else if (message instanceof PlayerLeftMessage leave) {
var player = leave.getPlayer(); var uuid = leave.getPlayer();
sessionScreen.removePlayer(player); var player = players.remove(uuid);
players.remove(player); log.info("Player {} left the session.", player.getName());
sessionScreen.removePlayer(uuid);
return Optional.empty(); return Optional.empty();
} else if (message instanceof PlayerModifiedMessage modified) { } else if (message instanceof PlayerModifiedMessage modified) {
var player = modified.getPlayer(); var player = modified.getPlayer();
log.info("Player {} was modified.", player.getName());
sessionScreen.modifyPlayer(player);
players.put(player.getUuid(), player); players.put(player.getUuid(), player);
sessionScreen.modifyPlayer(player);
if (self.equals(player.getUuid())) { if (self.equals(player.getUuid())) {
sessionScreen.setReady(player.isReady()); sessionScreen.setReady(player.isReady());
@ -113,20 +115,10 @@ public final class Session extends BaseState {
} }
private boolean isReady() { private boolean isReady() {
for (PlayerData player : players.values()) { return players.get(self).isReady();
if (self.equals(player.getUuid())) {
return player.isReady();
}
}
throw new NoSuchElementException();
} }
private String getName() { private String getName() {
for (PlayerData player : players.values()) { return players.get(self).getName();
if (self.equals(player.getUuid())) {
return player.getName();
}
}
throw new NoSuchElementException();
} }
} }

@ -1,4 +1,4 @@
info face="Enchanted Land" size=96 bold=0 italic=0 charset="" unicode=0 stretchH=100 smooth=1 aa=1 padding=4,4,4,4 spacing=-2,-2 info face="Enchanted Land" size=96 bold=0 italic=0 charset="" unicode=0 stretchH=100 smooth=1 aa=1 padding=8,-16,0,-16 spacing=40,40
common lineHeight=119 base=83 scaleW=2048 scaleH=1024 pages=1 packed=0 common lineHeight=119 base=83 scaleW=2048 scaleH=1024 pages=1 packed=0
page id=0 file="enchanted.png" page id=0 file="enchanted.png"
chars count=340 chars count=340

@ -49,7 +49,9 @@
"fontColor": "gold", "fontColor": "gold",
"downFontColor": "dark_gold", "downFontColor": "dark_gold",
"overFontColor": "darkened_gold", "overFontColor": "darkened_gold",
"disabledFontColor" : "darker_gold" "disabledFontColor" : "darker_gold",
"focused": "textbutton-focused",
"up": "textbutton"
}, },
"simple": { "simple": {
"font": "default-font", "font": "default-font",

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Loading…
Cancel
Save