From 48ed6df6b41a3ea60d196537200adeea8f05cd5f Mon Sep 17 00:00:00 2001 From: ramollia <> Date: Tue, 28 Oct 2025 12:59:15 +0100 Subject: [PATCH 1/7] started working on the widget system --- app/src/main/java/org/toop/app/App.java | 9 ++++-- app/src/main/java/org/toop/app/Test.java | 14 ++++++++++ .../org/toop/app/canvas/ReversiCanvas.java | 5 ++-- .../java/org/toop/app/game/ReversiGame.java | 14 ++++------ .../main/java/org/toop/app/widget/Widget.java | 7 +++++ .../org/toop/app/widget/WidgetSystem.java | 28 +++++++++++++++++++ .../toop/app/widget/complex/PopupWidget.java | 16 +++++++++++ .../app/widget/complex/PrimaryWidget.java | 16 +++++++++++ .../toop/app/widget/complex/ViewWidget.java | 28 +++++++++++++++++++ .../widget/simple/LabeledButtonWidget.java | 26 +++++++++++++++++ .../org/toop/app/widget/view/QuitView.java | 12 ++++++++ 11 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/org/toop/app/Test.java create mode 100644 app/src/main/java/org/toop/app/widget/Widget.java create mode 100644 app/src/main/java/org/toop/app/widget/WidgetSystem.java create mode 100644 app/src/main/java/org/toop/app/widget/complex/PopupWidget.java create mode 100644 app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java create mode 100644 app/src/main/java/org/toop/app/widget/complex/ViewWidget.java create mode 100644 app/src/main/java/org/toop/app/widget/simple/LabeledButtonWidget.java create mode 100644 app/src/main/java/org/toop/app/widget/view/QuitView.java diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index d9d1f7f..a24c518 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -3,6 +3,7 @@ package org.toop.app; import org.toop.app.view.ViewStack; import org.toop.app.view.views.MainView; import org.toop.app.view.views.QuitView; +import org.toop.app.widget.WidgetSystem; import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.resource.ResourceManager; @@ -30,14 +31,18 @@ public final class App extends Application { @Override public void start(Stage stage) throws Exception { - final StackPane root = new StackPane(); + final StackPane root = WidgetSystem.setup(); + final Scene scene = new Scene(root); ViewStack.setup(scene); + scene.setRoot(root); stage.setTitle(AppContext.getString("app-title")); stage.setWidth(1080); stage.setHeight(720); + scene.getRoot(); + stage.setOnCloseRequest(event -> { event.consume(); startQuit(); @@ -59,7 +64,7 @@ public final class App extends Application { AppSettings.applySettings(); new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent(); - ViewStack.push(new MainView()); + // ViewStack.push(new MainView()); } public static void startQuit() { diff --git a/app/src/main/java/org/toop/app/Test.java b/app/src/main/java/org/toop/app/Test.java new file mode 100644 index 0000000..5a4b40a --- /dev/null +++ b/app/src/main/java/org/toop/app/Test.java @@ -0,0 +1,14 @@ +//package org.toop.app; +// +//public class Quit { +// PopupWidget popup; +// Quit() { +// this.popup = new PopupWidget( +// new ConfirmationWidget( +// "are-you-sure", +// "yes", () -> App.quit(), +// "no", () -> popup.pop() +// ) +// ); +// } +//} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java index 8eec0ad..313e0e9 100644 --- a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java @@ -17,7 +17,8 @@ public final class ReversiCanvas extends GameCanvas { drawDot(Color.WHITE, 27); } - public void drawLegalPosition(int cell) { - drawDot(new Color(1.0f, 0.0f, 0.0f, 0.5f), cell); + public void drawLegalPosition(Color color, int cell) { + fill(new Color(color.getRed() * 0.25, color.getGreen() * 0.25, color.getBlue() * 0.25, 1.0), cell); + drawDot(new Color(color.getRed() * 0.5, color.getGreen() * 0.5, color.getBlue() * 0.5, 1.0), cell); } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/game/ReversiGame.java b/app/src/main/java/org/toop/app/game/ReversiGame.java index 64f0b6c..3645fb0 100644 --- a/app/src/main/java/org/toop/app/game/ReversiGame.java +++ b/app/src/main/java/org/toop/app/game/ReversiGame.java @@ -266,16 +266,14 @@ public final class ReversiGame { final SequentialTransition animation = new SequentialTransition(); isPaused.set(true); + final Color fromColor = game.getCurrentPlayer() == 0? Color.WHITE : Color.BLACK; + final Color toColor = game.getCurrentPlayer() == 0? Color.BLACK : Color.WHITE; + if (animate && flipped != null) { for (final Game.Move flip : flipped) { canvas.clear(flip.position()); - - final Color from = flip.value() == 'W' ? Color.BLACK : Color.WHITE; - final Color to = flip.value() == 'W' ? Color.WHITE : Color.BLACK; - - canvas.drawDot(from, flip.position()); - - animation.getChildren().addFirst(canvas.flipDot(from, to, flip.position())); + canvas.drawDot(fromColor, flip.position()); + animation.getChildren().addFirst(canvas.flipDot(fromColor, toColor, flip.position())); } } @@ -285,7 +283,7 @@ public final class ReversiGame { final Game.Move[] legalMoves = game.getLegalMoves(); for (final Game.Move legalMove : legalMoves) { - canvas.drawLegalPosition(legalMove.position()); + canvas.drawLegalPosition(toColor, legalMove.position()); } }); diff --git a/app/src/main/java/org/toop/app/widget/Widget.java b/app/src/main/java/org/toop/app/widget/Widget.java new file mode 100644 index 0000000..25b95cc --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/Widget.java @@ -0,0 +1,7 @@ +package org.toop.app.widget; + +import javafx.scene.Node; + +public interface Widget { + T getNode(); +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/WidgetSystem.java b/app/src/main/java/org/toop/app/widget/WidgetSystem.java new file mode 100644 index 0000000..d02cb67 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/WidgetSystem.java @@ -0,0 +1,28 @@ +package org.toop.app.widget; + +import javafx.geometry.Pos; +import javafx.scene.layout.StackPane; + +public final class WidgetSystem { + private static StackPane root; + + public static StackPane setup() { + if (root != null) { + return root; + } + + root = new StackPane(); + root.getStyleClass().add("bg-primary"); + + return root; + } + + public static void add(Pos position, Widget widget) { + StackPane.setAlignment(widget.getNode(), position); + root.getChildren().add(widget.getNode()); + } + + public static void remove(Widget widget) { + root.getChildren().remove(widget.getNode()); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/PopupWidget.java b/app/src/main/java/org/toop/app/widget/complex/PopupWidget.java new file mode 100644 index 0000000..9d44c80 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/PopupWidget.java @@ -0,0 +1,16 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.WidgetSystem; + +import javafx.geometry.Pos; + +public abstract class PopupWidget extends ViewWidget { + public PopupWidget() { + super("bg-popup"); + WidgetSystem.add(Pos.CENTER, this); + } + + public void pop() { + WidgetSystem.remove(this); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java b/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java new file mode 100644 index 0000000..462b30d --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java @@ -0,0 +1,16 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.WidgetSystem; + +import javafx.geometry.Pos; + +public abstract class PrimaryWidget extends ViewWidget { + public PrimaryWidget() { + super("bg-primary"); + } + + public void transition(PrimaryWidget primary) { + WidgetSystem.add(Pos.CENTER, primary); + WidgetSystem.remove(this); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java b/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java new file mode 100644 index 0000000..dd6c9fb --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java @@ -0,0 +1,28 @@ +package org.toop.app.widget.complex; + +import javafx.geometry.Pos; +import org.toop.app.widget.Widget; + +import javafx.scene.layout.StackPane; + +public abstract class ViewWidget extends StackPane implements Widget { + public ViewWidget(String cssClass) { + getStyleClass().add(cssClass); + } + + public void add(Pos position, Widget widget) { + setAlignment(widget.getNode(), position); + getChildren().add(widget.getNode()); + } + + @Override + public StackPane getNode() { + return this; + } + + public void show() { + + } + + public abstract void reload(); +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/simple/LabeledButtonWidget.java b/app/src/main/java/org/toop/app/widget/simple/LabeledButtonWidget.java new file mode 100644 index 0000000..8436f95 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/simple/LabeledButtonWidget.java @@ -0,0 +1,26 @@ +package org.toop.app.widget.simple; + +import org.toop.app.widget.Widget; + +import javafx.scene.control.Button; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +public class LabeledButtonWidget extends VBox implements Widget { + public LabeledButtonWidget( + String labelText, + String buttonText, Runnable buttonOnAction + ) { + var text = new Text(labelText); + + var button = new Button(buttonText); + button.setOnAction(_ -> buttonOnAction.run()); + + super(text, button); + } + + @Override + public VBox getNode() { + return this; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/view/QuitView.java b/app/src/main/java/org/toop/app/widget/view/QuitView.java new file mode 100644 index 0000000..bbd977c --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/view/QuitView.java @@ -0,0 +1,12 @@ +package org.toop.app.widget.view; + +import org.toop.app.widget.complex.PopupWidget; + +public class QuitView extends PopupWidget { + protected QuitView() { + } + + @Override + public void reload() { + } +} \ No newline at end of file From 84c17d185bd461777215011576ba48dea86569a2 Mon Sep 17 00:00:00 2001 From: Bas de Jong Date: Tue, 28 Oct 2025 14:37:15 +0100 Subject: [PATCH 2/7] Fixes for garbage code by Omar --- app/src/main/java/org/toop/app/App.java | 32 +++++++--- .../java/org/toop/app/interfaces/Popup.java | 6 ++ .../java/org/toop/app/widget/Primitive.java | 63 +++++++++++++++++++ .../main/java/org/toop/app/widget/Widget.java | 9 +++ ...WidgetSystem.java => WidgetContainer.java} | 2 +- .../app/widget/complex/ConfirmWidget.java | 26 ++++++++ .../toop/app/widget/complex/PopupWidget.java | 12 ++-- .../app/widget/complex/PrimaryWidget.java | 20 +++--- .../widget/complex/TransitionAnimation.java | 5 ++ .../toop/app/widget/complex/ViewWidget.java | 17 +++-- .../org/toop/app/widget/view/MainView.java | 4 ++ 11 files changed, 164 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/org/toop/app/interfaces/Popup.java create mode 100644 app/src/main/java/org/toop/app/widget/Primitive.java rename app/src/main/java/org/toop/app/widget/{WidgetSystem.java => WidgetContainer.java} (93%) create mode 100644 app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java create mode 100644 app/src/main/java/org/toop/app/widget/complex/TransitionAnimation.java create mode 100644 app/src/main/java/org/toop/app/widget/view/MainView.java diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index a24c518..6072589 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -1,9 +1,11 @@ package org.toop.app; +import javafx.geometry.Pos; import org.toop.app.view.ViewStack; -import org.toop.app.view.views.MainView; import org.toop.app.view.views.QuitView; -import org.toop.app.widget.WidgetSystem; +import org.toop.app.widget.WidgetContainer; +import org.toop.app.widget.complex.ConfirmWidget; +import org.toop.app.widget.complex.PopupWidget; import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.resource.ResourceManager; @@ -16,6 +18,8 @@ import javafx.scene.Scene; import javafx.scene.layout.StackPane; import javafx.stage.Stage; +import java.util.HashMap; + public final class App extends Application { private static Stage stage; private static Scene scene; @@ -31,11 +35,8 @@ public final class App extends Application { @Override public void start(Stage stage) throws Exception { - final StackPane root = WidgetSystem.setup(); - + final StackPane root = WidgetContainer.setup(); final Scene scene = new Scene(root); - ViewStack.setup(scene); - scene.setRoot(root); stage.setTitle(AppContext.getString("app-title")); stage.setWidth(1080); @@ -64,7 +65,22 @@ public final class App extends Application { AppSettings.applySettings(); new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent(); - // ViewStack.push(new MainView()); + var abc = new ConfirmWidget("abc"); + var cab = new ConfirmWidget("cab"); + + abc.addButton("test", () -> { + WidgetContainer.add(Pos.CENTER, cab); + WidgetContainer.remove(abc); + }); + abc.addButton("test3333", () -> IO.println("Second test works!")); + + cab.addButton("cab321312", () -> IO.println("Third test")); + cab.addButton("cab31232132131", () -> { + IO.println("Fourth test"); + WidgetContainer.remove(cab); + }); + + WidgetContainer.add(Pos.CENTER, abc); } public static void startQuit() { @@ -89,7 +105,7 @@ public final class App extends Application { public static void reload() { stage.setTitle(AppContext.getString("app-title")); - ViewStack.reload(); + //ViewStack.reload(); } public static void setFullscreen(boolean fullscreen) { diff --git a/app/src/main/java/org/toop/app/interfaces/Popup.java b/app/src/main/java/org/toop/app/interfaces/Popup.java new file mode 100644 index 0000000..7a8b3dd --- /dev/null +++ b/app/src/main/java/org/toop/app/interfaces/Popup.java @@ -0,0 +1,6 @@ +package org.toop.app.interfaces; + +public interface Popup { + void push(); + void pop(); +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/Primitive.java b/app/src/main/java/org/toop/app/widget/Primitive.java new file mode 100644 index 0000000..262d16d --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/Primitive.java @@ -0,0 +1,63 @@ +package org.toop.app.widget; + +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +public final class Primitive { + public static Text header(String label) { + var header = new Text(label); + header.getStyleClass().add("header"); + return header; + } + + public static Text text(String label) { + var text = new Text(label); + text.getStyleClass().add("text"); + return text; + } + + public static Button button(String label) { + var button = new Button(label); + button.getStyleClass().add("button"); + return button; + } + + public static TextField input() { + var input = new TextField(); + input.getStyleClass().add("input"); + return input; + } + + public static Slider slider() { + var slider = new Slider(); + slider.getStyleClass().add("slider"); + return slider; + } + + public static ComboBox choice() { + var choice = new ComboBox(); + choice.getStyleClass().add("choice"); + return choice; + } + + public static ScrollPane scroll(Node content) { + var scroll = new ScrollPane(content); + scroll.getStyleClass().add("scroll"); + return scroll; + } + + public static HBox hbox(Node... nodes) { + var hbox = new HBox(nodes); + hbox.getStyleClass().add("container"); + return hbox; + } + + public static VBox vbox(Node... nodes) { + var vbox = new VBox(nodes); + vbox.getStyleClass().add("container"); + return vbox; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/Widget.java b/app/src/main/java/org/toop/app/widget/Widget.java index 25b95cc..e543b4e 100644 --- a/app/src/main/java/org/toop/app/widget/Widget.java +++ b/app/src/main/java/org/toop/app/widget/Widget.java @@ -1,7 +1,16 @@ package org.toop.app.widget; +import javafx.geometry.Pos; import javafx.scene.Node; public interface Widget { T getNode(); + + default void show(Pos position) { + WidgetContainer.add(position, this); + } + + default void hide() { + WidgetContainer.remove(this); + } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/WidgetSystem.java b/app/src/main/java/org/toop/app/widget/WidgetContainer.java similarity index 93% rename from app/src/main/java/org/toop/app/widget/WidgetSystem.java rename to app/src/main/java/org/toop/app/widget/WidgetContainer.java index d02cb67..c0d62a8 100644 --- a/app/src/main/java/org/toop/app/widget/WidgetSystem.java +++ b/app/src/main/java/org/toop/app/widget/WidgetContainer.java @@ -3,7 +3,7 @@ package org.toop.app.widget; import javafx.geometry.Pos; import javafx.scene.layout.StackPane; -public final class WidgetSystem { +public final class WidgetContainer { private static StackPane root; public static StackPane setup() { diff --git a/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java b/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java new file mode 100644 index 0000000..c570e38 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java @@ -0,0 +1,26 @@ +package org.toop.app.widget.complex; + +import javafx.scene.layout.HBox; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.Widget; + +import javafx.scene.layout.VBox; + +public class ConfirmWidget implements Widget { + private final HBox buttonsContainer; + private final VBox container; + + public ConfirmWidget(String confirm) { + buttonsContainer = Primitive.hbox(); + container = Primitive.vbox(Primitive.text(confirm), buttonsContainer); + } + + public void addButton(String label, Runnable onClick) { + var button = Primitive.button(label); + button.setOnAction(_ -> onClick.run()); + buttonsContainer.getChildren().add(button); + } + + @Override + public VBox getNode() { return container; } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/PopupWidget.java b/app/src/main/java/org/toop/app/widget/complex/PopupWidget.java index 9d44c80..66448d9 100644 --- a/app/src/main/java/org/toop/app/widget/complex/PopupWidget.java +++ b/app/src/main/java/org/toop/app/widget/complex/PopupWidget.java @@ -1,16 +1,20 @@ package org.toop.app.widget.complex; -import org.toop.app.widget.WidgetSystem; +import org.toop.app.interfaces.Popup; +import org.toop.app.widget.WidgetContainer; import javafx.geometry.Pos; -public abstract class PopupWidget extends ViewWidget { +public abstract class PopupWidget extends ViewWidget implements Popup { public PopupWidget() { super("bg-popup"); - WidgetSystem.add(Pos.CENTER, this); } + public void push() { + WidgetContainer.add(Pos.CENTER, this); + } + public void pop() { - WidgetSystem.remove(this); + WidgetContainer.remove(this); } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java b/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java index 462b30d..4ba7dc3 100644 --- a/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java +++ b/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java @@ -1,16 +1,16 @@ package org.toop.app.widget.complex; -import org.toop.app.widget.WidgetSystem; - import javafx.geometry.Pos; +import org.toop.app.widget.WidgetContainer; -public abstract class PrimaryWidget extends ViewWidget { - public PrimaryWidget() { - super("bg-primary"); - } +public abstract class PrimaryWidget extends ViewWidget implements TransitionAnimation { + public PrimaryWidget() { + super("bg-primary"); + } - public void transition(PrimaryWidget primary) { - WidgetSystem.add(Pos.CENTER, primary); - WidgetSystem.remove(this); - } + @Override + public void transition(PrimaryWidget primary) { + WidgetContainer.add(Pos.CENTER, primary); + WidgetContainer.remove(this); + } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/TransitionAnimation.java b/app/src/main/java/org/toop/app/widget/complex/TransitionAnimation.java new file mode 100644 index 0000000..001ff29 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/TransitionAnimation.java @@ -0,0 +1,5 @@ +package org.toop.app.widget.complex; + +public interface TransitionAnimation { + void transition(PrimaryWidget primary); +} diff --git a/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java b/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java index dd6c9fb..356af91 100644 --- a/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java +++ b/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java @@ -5,23 +5,22 @@ import org.toop.app.widget.Widget; import javafx.scene.layout.StackPane; -public abstract class ViewWidget extends StackPane implements Widget { +public abstract class ViewWidget implements Widget { + private final StackPane container; + public ViewWidget(String cssClass) { - getStyleClass().add(cssClass); + container = new StackPane(); + container.getStyleClass().add(cssClass); } public void add(Pos position, Widget widget) { - setAlignment(widget.getNode(), position); - getChildren().add(widget.getNode()); + StackPane.setAlignment(widget.getNode(), position); + container.getChildren().add(widget.getNode()); } @Override public StackPane getNode() { - return this; - } - - public void show() { - + return container; } public abstract void reload(); diff --git a/app/src/main/java/org/toop/app/widget/view/MainView.java b/app/src/main/java/org/toop/app/widget/view/MainView.java new file mode 100644 index 0000000..1d77b52 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/view/MainView.java @@ -0,0 +1,4 @@ +package org.toop.app.widget.view; + +public class MainView { +} \ No newline at end of file From b84255e00ee8934dc9f15b7d5081540141162f2e Mon Sep 17 00:00:00 2001 From: Bas de Jong Date: Tue, 28 Oct 2025 15:23:36 +0100 Subject: [PATCH 3/7] Added replace to reduce boiler plate code --- app/src/main/java/org/toop/app/App.java | 5 ++--- app/src/main/java/org/toop/app/widget/Widget.java | 5 +++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index 6072589..8f49113 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -69,15 +69,14 @@ public final class App extends Application { var cab = new ConfirmWidget("cab"); abc.addButton("test", () -> { - WidgetContainer.add(Pos.CENTER, cab); - WidgetContainer.remove(abc); + abc.replace(cab, Pos.CENTER); }); + abc.addButton("test3333", () -> IO.println("Second test works!")); cab.addButton("cab321312", () -> IO.println("Third test")); cab.addButton("cab31232132131", () -> { IO.println("Fourth test"); - WidgetContainer.remove(cab); }); WidgetContainer.add(Pos.CENTER, abc); diff --git a/app/src/main/java/org/toop/app/widget/Widget.java b/app/src/main/java/org/toop/app/widget/Widget.java index e543b4e..f5de6a2 100644 --- a/app/src/main/java/org/toop/app/widget/Widget.java +++ b/app/src/main/java/org/toop/app/widget/Widget.java @@ -13,4 +13,9 @@ public interface Widget { default void hide() { WidgetContainer.remove(this); } + + default void replace(Widget newWidget, Pos newWidgetPosition) { + this.hide(); + newWidget.show(newWidgetPosition); + } } \ No newline at end of file From 1c9af582640254929e96f996348da94e5420301e Mon Sep 17 00:00:00 2001 From: ramollia <> Date: Fri, 31 Oct 2025 17:33:19 +0100 Subject: [PATCH 4/7] half done with the widget system --- app/src/main/java/org/toop/app/App.java | 54 ++---- app/src/main/java/org/toop/app/Test.java | 14 -- .../java/org/toop/app/interfaces/Popup.java | 6 - .../toop/app/view/displays/SongDisplay.java | 9 +- .../org/toop/app/view/views/OptionsView.java | 1 - .../java/org/toop/app/widget/Primitive.java | 179 +++++++++++++----- .../main/java/org/toop/app/widget/Widget.java | 10 +- .../org/toop/app/widget/WidgetContainer.java | 54 +++++- .../app/widget/complex/ConfirmWidget.java | 35 ++-- .../widget/complex/LabeledChoiceWidget.java | 42 ++++ .../widget/complex/LabeledInputWidget.java | 34 ++++ .../widget/complex/LabeledSliderWidget.java | 49 +++++ .../toop/app/widget/complex/PopupWidget.java | 15 +- .../app/widget/complex/PrimaryWidget.java | 41 +++- .../toop/app/widget/complex/StackWidget.java | 47 +++++ .../toop/app/widget/complex/ToggleWidget.java | 62 ++++++ .../widget/complex/TransitionAnimation.java | 5 - .../toop/app/widget/complex/ViewWidget.java | 27 --- .../toop/app/widget/display/SongDisplay.java | 117 ++++++++++++ .../org/toop/app/widget/popup/QuitPopup.java | 24 +++ .../app/widget/primary/CreditsPrimary.java | 81 ++++++++ .../toop/app/widget/primary/LocalPrimary.java | 25 +++ .../toop/app/widget/primary/MainPrimary.java | 39 ++++ .../app/widget/primary/OnlinePrimary.java | 38 ++++ .../app/widget/primary/OptionsPrimary.java | 161 ++++++++++++++++ .../widget/simple/LabeledButtonWidget.java | 26 --- .../org/toop/app/widget/view/MainView.java | 4 - .../org/toop/app/widget/view/QuitView.java | 12 -- .../main/java/org/toop/local/AppContext.java | 21 +- .../main/resources/assets/style/general.css | 21 +- 30 files changed, 1009 insertions(+), 244 deletions(-) delete mode 100644 app/src/main/java/org/toop/app/Test.java delete mode 100644 app/src/main/java/org/toop/app/interfaces/Popup.java create mode 100644 app/src/main/java/org/toop/app/widget/complex/LabeledChoiceWidget.java create mode 100644 app/src/main/java/org/toop/app/widget/complex/LabeledInputWidget.java create mode 100644 app/src/main/java/org/toop/app/widget/complex/LabeledSliderWidget.java create mode 100644 app/src/main/java/org/toop/app/widget/complex/StackWidget.java create mode 100644 app/src/main/java/org/toop/app/widget/complex/ToggleWidget.java delete mode 100644 app/src/main/java/org/toop/app/widget/complex/TransitionAnimation.java delete mode 100644 app/src/main/java/org/toop/app/widget/complex/ViewWidget.java create mode 100644 app/src/main/java/org/toop/app/widget/display/SongDisplay.java create mode 100644 app/src/main/java/org/toop/app/widget/popup/QuitPopup.java create mode 100644 app/src/main/java/org/toop/app/widget/primary/CreditsPrimary.java create mode 100644 app/src/main/java/org/toop/app/widget/primary/LocalPrimary.java create mode 100644 app/src/main/java/org/toop/app/widget/primary/MainPrimary.java create mode 100644 app/src/main/java/org/toop/app/widget/primary/OnlinePrimary.java create mode 100644 app/src/main/java/org/toop/app/widget/primary/OptionsPrimary.java delete mode 100644 app/src/main/java/org/toop/app/widget/simple/LabeledButtonWidget.java delete mode 100644 app/src/main/java/org/toop/app/widget/view/MainView.java delete mode 100644 app/src/main/java/org/toop/app/widget/view/QuitView.java diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index 8f49113..db6ec38 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -1,11 +1,10 @@ package org.toop.app; -import javafx.geometry.Pos; -import org.toop.app.view.ViewStack; -import org.toop.app.view.views.QuitView; +import org.toop.app.widget.Widget; import org.toop.app.widget.WidgetContainer; -import org.toop.app.widget.complex.ConfirmWidget; -import org.toop.app.widget.complex.PopupWidget; +import org.toop.app.widget.display.SongDisplay; +import org.toop.app.widget.popup.QuitPopup; +import org.toop.app.widget.primary.MainPrimary; import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.resource.ResourceManager; @@ -14,12 +13,11 @@ import org.toop.local.AppContext; import org.toop.local.AppSettings; import javafx.application.Application; +import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.layout.StackPane; import javafx.stage.Stage; -import java.util.HashMap; - public final class App extends Application { private static Stage stage; private static Scene scene; @@ -39,6 +37,8 @@ public final class App extends Application { final Scene scene = new Scene(root); stage.setTitle(AppContext.getString("app-title")); + stage.titleProperty().bind(AppContext.bindToKey("app-title")); + stage.setWidth(1080); stage.setHeight(720); @@ -65,21 +65,8 @@ public final class App extends Application { AppSettings.applySettings(); new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent(); - var abc = new ConfirmWidget("abc"); - var cab = new ConfirmWidget("cab"); - - abc.addButton("test", () -> { - abc.replace(cab, Pos.CENTER); - }); - - abc.addButton("test3333", () -> IO.println("Second test works!")); - - cab.addButton("cab321312", () -> IO.println("Third test")); - cab.addButton("cab31232132131", () -> { - IO.println("Fourth test"); - }); - - WidgetContainer.add(Pos.CENTER, abc); + WidgetContainer.add(Pos.CENTER, new MainPrimary()); + WidgetContainer.add(Pos.BOTTOM_RIGHT, new SongDisplay()); } public static void startQuit() { @@ -87,47 +74,32 @@ public final class App extends Application { return; } - ViewStack.push(new QuitView()); + WidgetContainer.add(Pos.CENTER, new QuitPopup()); isQuitting = true; } public static void stopQuit() { - ViewStack.pop(); isQuitting = false; } public static void quit() { - ViewStack.cleanup(); stage.close(); System.exit(0); // TODO: This is like dropping a nuke } - public static void reload() { - stage.setTitle(AppContext.getString("app-title")); - //ViewStack.reload(); - } - public static void setFullscreen(boolean fullscreen) { stage.setFullScreen(fullscreen); - width = (int) stage.getWidth(); - height = (int) stage.getHeight(); - - reload(); + width = (int)stage.getWidth(); + height = (int)stage.getHeight(); } public static void setStyle(String theme, String layoutSize) { - final int stylesCount = scene.getStylesheets().size(); - - for (int i = 0; i < stylesCount; i++) { - scene.getStylesheets().removeLast(); - } + scene.getStylesheets().clear(); scene.getStylesheets().add(ResourceManager.get("general.css").getUrl()); scene.getStylesheets().add(ResourceManager.get(theme + ".css").getUrl()); scene.getStylesheets().add(ResourceManager.get(layoutSize + ".css").getUrl()); - - reload(); } public static int getWidth() { diff --git a/app/src/main/java/org/toop/app/Test.java b/app/src/main/java/org/toop/app/Test.java deleted file mode 100644 index 5a4b40a..0000000 --- a/app/src/main/java/org/toop/app/Test.java +++ /dev/null @@ -1,14 +0,0 @@ -//package org.toop.app; -// -//public class Quit { -// PopupWidget popup; -// Quit() { -// this.popup = new PopupWidget( -// new ConfirmationWidget( -// "are-you-sure", -// "yes", () -> App.quit(), -// "no", () -> popup.pop() -// ) -// ); -// } -//} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/interfaces/Popup.java b/app/src/main/java/org/toop/app/interfaces/Popup.java deleted file mode 100644 index 7a8b3dd..0000000 --- a/app/src/main/java/org/toop/app/interfaces/Popup.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.toop.app.interfaces; - -public interface Popup { - void push(); - void pop(); -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/view/displays/SongDisplay.java b/app/src/main/java/org/toop/app/view/displays/SongDisplay.java index 0c31455..8e9da5d 100644 --- a/app/src/main/java/org/toop/app/view/displays/SongDisplay.java +++ b/app/src/main/java/org/toop/app/view/displays/SongDisplay.java @@ -1,11 +1,13 @@ package org.toop.app.view.displays; import javafx.application.Platform; +import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ProgressBar; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; +import org.toop.app.widget.Widget; import org.toop.framework.audio.AudioEventListener; import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.eventbus.EventFlow; @@ -13,7 +15,7 @@ import javafx.geometry.Pos; import javafx.scene.text.Text; import org.toop.framework.eventbus.GlobalEventBus; -public class SongDisplay extends VBox { +public class SongDisplay extends VBox implements Widget { private final Text songTitle; private final ProgressBar progressBar; @@ -107,6 +109,11 @@ public class SongDisplay extends VBox { String time = positionMinutes + ":" + positionSecondsStr + " / " + durationMinutes + ":" + durationSecondsStr; return time; } + + @Override + public Node getNode() { + return this; + } } diff --git a/app/src/main/java/org/toop/app/view/views/OptionsView.java b/app/src/main/java/org/toop/app/view/views/OptionsView.java index d4af353..08cdfa0 100644 --- a/app/src/main/java/org/toop/app/view/views/OptionsView.java +++ b/app/src/main/java/org/toop/app/view/views/OptionsView.java @@ -126,7 +126,6 @@ public final class OptionsView extends View { languageCombobox.getSelectionModel().selectedItemProperty().addListener((_, _, newValue) -> { AppSettings.getSettings().setLocale(newValue.toString()); AppContext.setLocale(newValue); - App.reload(); }); languageCombobox.setConverter(new StringConverter<>() { diff --git a/app/src/main/java/org/toop/app/widget/Primitive.java b/app/src/main/java/org/toop/app/widget/Primitive.java index 262d16d..7fe8832 100644 --- a/app/src/main/java/org/toop/app/widget/Primitive.java +++ b/app/src/main/java/org/toop/app/widget/Primitive.java @@ -1,63 +1,150 @@ package org.toop.app.widget; +import org.toop.local.AppContext; + +import java.util.function.Consumer; + +import javafx.collections.FXCollections; import javafx.scene.Node; -import javafx.scene.control.*; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Separator; +import javafx.scene.control.Slider; +import javafx.scene.control.TextField; import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.text.Text; +import javafx.util.StringConverter; public final class Primitive { - public static Text header(String label) { - var header = new Text(label); - header.getStyleClass().add("header"); - return header; - } + public static Text header(String key) { + var header = new Text(); + header.getStyleClass().add("header"); - public static Text text(String label) { - var text = new Text(label); - text.getStyleClass().add("text"); - return text; - } + header.setText(AppContext.getString(key)); + header.textProperty().bind(AppContext.bindToKey(key)); - public static Button button(String label) { - var button = new Button(label); - button.getStyleClass().add("button"); - return button; - } + return header; + } - public static TextField input() { - var input = new TextField(); - input.getStyleClass().add("input"); - return input; - } + public static Text text(String key) { + var text = new Text(); + text.getStyleClass().add("text"); - public static Slider slider() { - var slider = new Slider(); - slider.getStyleClass().add("slider"); - return slider; - } + text.setText(AppContext.getString(key)); + text.textProperty().bind(AppContext.bindToKey(key)); - public static ComboBox choice() { - var choice = new ComboBox(); - choice.getStyleClass().add("choice"); - return choice; - } + return text; + } - public static ScrollPane scroll(Node content) { - var scroll = new ScrollPane(content); - scroll.getStyleClass().add("scroll"); - return scroll; - } + public static Button button(String key, Runnable onAction) { + var button = new Button(); + button.getStyleClass().add("button"); - public static HBox hbox(Node... nodes) { - var hbox = new HBox(nodes); - hbox.getStyleClass().add("container"); - return hbox; - } + button.setText(AppContext.getString(key)); + button.textProperty().bind(AppContext.bindToKey(key)); - public static VBox vbox(Node... nodes) { - var vbox = new VBox(nodes); - vbox.getStyleClass().add("container"); - return vbox; - } + if (onAction != null) { + button.setOnAction(_ -> + onAction.run()); + } + + return button; + } + + public static TextField input(String promptKey, String text, Consumer onValueChanged) { + var input = new TextField(); + input.getStyleClass().add("input"); + + input.setPromptText(AppContext.getString(promptKey)); + input.promptTextProperty().bind(AppContext.bindToKey(promptKey)); + + input.setText(text); + + if (onValueChanged != null) { + input.textProperty().addListener((_, _, newValue) -> + onValueChanged.accept(newValue)); + } + + return input; + } + + public static Slider slider(int min, int max, int value, Consumer onValueChanged) { + var slider = new Slider(); + slider.getStyleClass().add("slider"); + + slider.setMin(min); + slider.setMax(max); + slider.setValue(value); + + if (onValueChanged != null) { + slider.valueProperty().addListener((_, _, newValue) -> + onValueChanged.accept(newValue.intValue())); + } + + return slider; + } + + @SafeVarargs + public static ComboBox choice(StringConverter converter, T value, Consumer onValueChanged, T... items) { + var choice = new ComboBox(); + choice.getStyleClass().add("choice"); + + if (converter != null) { + choice.setConverter(converter); + } + + if (value != null) { + choice.setValue(value); + } + + if (onValueChanged != null) { + choice.valueProperty().addListener((_, _, newValue) -> + onValueChanged.accept(newValue)); + } + + choice.setItems(FXCollections.observableArrayList(items)); + + return choice; + } + + public static ScrollPane scroll(Node content) { + var scroll = new ScrollPane(); + scroll.getStyleClass().add("scroll"); + scroll.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + scroll.setFitToWidth(true); + + scroll.setContent(content); + + return scroll; + } + + public static Separator separator() { + var separator = new Separator(); + separator.getStyleClass().add("separator"); + + return separator; + } + + public static HBox hbox(Node... nodes) { + var hbox = new HBox(); + hbox.getStyleClass().add("container"); + hbox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + + hbox.getChildren().addAll(nodes); + + return hbox; + } + + public static VBox vbox(Node... nodes) { + var vbox = new VBox(); + vbox.getStyleClass().add("container"); + vbox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + + vbox.getChildren().addAll(nodes); + + return vbox; + } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/Widget.java b/app/src/main/java/org/toop/app/widget/Widget.java index f5de6a2..5f7a269 100644 --- a/app/src/main/java/org/toop/app/widget/Widget.java +++ b/app/src/main/java/org/toop/app/widget/Widget.java @@ -3,8 +3,8 @@ package org.toop.app.widget; import javafx.geometry.Pos; import javafx.scene.Node; -public interface Widget { - T getNode(); +public interface Widget { + Node getNode(); default void show(Pos position) { WidgetContainer.add(position, this); @@ -14,8 +14,8 @@ public interface Widget { WidgetContainer.remove(this); } - default void replace(Widget newWidget, Pos newWidgetPosition) { - this.hide(); - newWidget.show(newWidgetPosition); + default void replace(Pos position, Widget widget) { + widget.show(position); + hide(); } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/WidgetContainer.java b/app/src/main/java/org/toop/app/widget/WidgetContainer.java index c0d62a8..1d56c43 100644 --- a/app/src/main/java/org/toop/app/widget/WidgetContainer.java +++ b/app/src/main/java/org/toop/app/widget/WidgetContainer.java @@ -1,12 +1,21 @@ package org.toop.app.widget; +import org.toop.app.widget.complex.PopupWidget; +import org.toop.app.widget.complex.PrimaryWidget; + +import java.util.ArrayDeque; +import java.util.Deque; + +import javafx.application.Platform; import javafx.geometry.Pos; import javafx.scene.layout.StackPane; public final class WidgetContainer { + private static final Deque popups = new ArrayDeque<>(); + private static StackPane root; - public static StackPane setup() { + public static synchronized StackPane setup() { if (root != null) { return root; } @@ -17,12 +26,45 @@ public final class WidgetContainer { return root; } - public static void add(Pos position, Widget widget) { - StackPane.setAlignment(widget.getNode(), position); - root.getChildren().add(widget.getNode()); + public static void add(Pos position, Widget widget) { + if (root == null || widget == null) { + return; + } + + Platform.runLater(() -> { + if (root.getChildren().contains(widget.getNode())) { + return; + } + + StackPane.setAlignment(widget.getNode(), position); + + if (widget instanceof PrimaryWidget) { + root.getChildren().addFirst(widget.getNode()); + } else { + root.getChildren().add(widget.getNode()); + } + + if (widget instanceof PopupWidget popup) { + popups.push(popup); + } + }); } - public static void remove(Widget widget) { - root.getChildren().remove(widget.getNode()); + public static void remove(Widget widget) { + if (root == null || widget == null) { + return; + } + + Platform.runLater(() -> { + root.getChildren().remove(widget.getNode()); + + if (widget instanceof PrimaryWidget) { + for (var popup : popups) { + root.getChildren().remove(popup.getNode()); + } + + popups.clear(); + } + }); } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java b/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java index c570e38..107429c 100644 --- a/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java +++ b/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java @@ -1,26 +1,31 @@ package org.toop.app.widget.complex; -import javafx.scene.layout.HBox; import org.toop.app.widget.Primitive; import org.toop.app.widget.Widget; +import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; -public class ConfirmWidget implements Widget { - private final HBox buttonsContainer; - private final VBox container; +public class ConfirmWidget implements Widget { + private final HBox buttonsContainer; + private final VBox container; - public ConfirmWidget(String confirm) { - buttonsContainer = Primitive.hbox(); - container = Primitive.vbox(Primitive.text(confirm), buttonsContainer); - } + public ConfirmWidget(String confirm) { + buttonsContainer = Primitive.hbox(); + container = Primitive.vbox(Primitive.header(confirm), buttonsContainer); + } - public void addButton(String label, Runnable onClick) { - var button = Primitive.button(label); - button.setOnAction(_ -> onClick.run()); - buttonsContainer.getChildren().add(button); - } + public void addButton(String key, Runnable onClick) { + Platform.runLater(() -> { + var button = Primitive.button(key, onClick); + buttonsContainer.getChildren().add(button); + }); + } - @Override - public VBox getNode() { return container; } + @Override + public Node getNode() { + return container; + } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/LabeledChoiceWidget.java b/app/src/main/java/org/toop/app/widget/complex/LabeledChoiceWidget.java new file mode 100644 index 0000000..a59f6ed --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/LabeledChoiceWidget.java @@ -0,0 +1,42 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.Widget; + +import java.util.function.Consumer; + +import javafx.scene.Node; +import javafx.scene.control.ComboBox; +import javafx.scene.layout.VBox; +import javafx.util.StringConverter; + +public class LabeledChoiceWidget implements Widget { + private final ComboBox comboBox; + private final VBox container; + + @SafeVarargs + public LabeledChoiceWidget( + String key, + StringConverter converter, + T initialValue, + Consumer onValueChanged, + T... items + ) { + var label = Primitive.text(key); + comboBox = Primitive.choice(converter, initialValue, onValueChanged, items); + container = Primitive.vbox(label, comboBox); + } + + public T getValue() { + return comboBox.getValue(); + } + + public void setValue(T value) { + comboBox.setValue(value); + } + + @Override + public Node getNode() { + return container; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/LabeledInputWidget.java b/app/src/main/java/org/toop/app/widget/complex/LabeledInputWidget.java new file mode 100644 index 0000000..1223448 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/LabeledInputWidget.java @@ -0,0 +1,34 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.Widget; + +import java.util.function.Consumer; + +import javafx.scene.Node; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; + +public class LabeledInputWidget implements Widget { + private final TextField input; + private final VBox container; + + public LabeledInputWidget(String key, String promptKey, String initialText, Consumer onValueChanged) { + var label = Primitive.text(key); + input = Primitive.input(promptKey, initialText, onValueChanged); + container = Primitive.vbox(label, input); + } + + public String getValue() { + return input.getText(); + } + + public void setValue(String text) { + input.setText(text); + } + + @Override + public Node getNode() { + return container; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/LabeledSliderWidget.java b/app/src/main/java/org/toop/app/widget/complex/LabeledSliderWidget.java new file mode 100644 index 0000000..b770b1e --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/LabeledSliderWidget.java @@ -0,0 +1,49 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.Widget; + +import java.util.function.Consumer; + +import javafx.scene.Node; +import javafx.scene.control.Slider; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +public class LabeledSliderWidget implements Widget { + private final Slider slider; + private final Text labelValue; + private final VBox container; + + public LabeledSliderWidget(String key, int min, int max, int value, Consumer onValueChanged) { + var label = Primitive.text(key); + + labelValue = new Text(String.valueOf(value)); + labelValue.getStyleClass().add("text"); + + slider = Primitive.slider(min, max, value, newValue -> { + labelValue.setText(String.valueOf(newValue)); + + if (onValueChanged != null) { + onValueChanged.accept(newValue); + } + }); + + var sliderRow = Primitive.hbox(slider, labelValue); + container = Primitive.vbox(label, sliderRow); + } + + public int getValue() { + return (int)slider.getValue(); + } + + public void setValue(int newValue) { + slider.setValue(newValue); + labelValue.setText(String.valueOf(newValue)); + } + + @Override + public Node getNode() { + return container; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/PopupWidget.java b/app/src/main/java/org/toop/app/widget/complex/PopupWidget.java index 66448d9..853bf88 100644 --- a/app/src/main/java/org/toop/app/widget/complex/PopupWidget.java +++ b/app/src/main/java/org/toop/app/widget/complex/PopupWidget.java @@ -1,20 +1,7 @@ package org.toop.app.widget.complex; -import org.toop.app.interfaces.Popup; -import org.toop.app.widget.WidgetContainer; - -import javafx.geometry.Pos; - -public abstract class PopupWidget extends ViewWidget implements Popup { +public abstract class PopupWidget extends StackWidget { public PopupWidget() { super("bg-popup"); } - - public void push() { - WidgetContainer.add(Pos.CENTER, this); - } - - public void pop() { - WidgetContainer.remove(this); - } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java b/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java index 4ba7dc3..848e76e 100644 --- a/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java +++ b/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java @@ -1,16 +1,43 @@ package org.toop.app.widget.complex; import javafx.geometry.Pos; -import org.toop.app.widget.WidgetContainer; +import org.toop.app.widget.Primitive; + +public abstract class PrimaryWidget extends StackWidget { + private PrimaryWidget previous = null; -public abstract class PrimaryWidget extends ViewWidget implements TransitionAnimation { public PrimaryWidget() { super("bg-primary"); } - @Override - public void transition(PrimaryWidget primary) { - WidgetContainer.add(Pos.CENTER, primary); - WidgetContainer.remove(this); - } + public void transitionNext(PrimaryWidget primary) { + primary.previous = this; + replace(Pos.CENTER, primary); + + var backButton = Primitive.button("back", () -> { + primary.transitionPrevious(); + }); + + primary.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton)); + } + + public void transitionPrevious() { + if (previous == null) { + return; + } + + replace(Pos.CENTER, previous); + previous = null; + } + + public void reload(PrimaryWidget primary) { + primary.previous = previous; + replace(Pos.CENTER, primary); + + var backButton = Primitive.button("back", () -> { + primary.transitionPrevious(); + }); + + primary.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton)); + } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/StackWidget.java b/app/src/main/java/org/toop/app/widget/complex/StackWidget.java new file mode 100644 index 0000000..1bb70ff --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/StackWidget.java @@ -0,0 +1,47 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.Widget; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.layout.StackPane; + +public abstract class StackWidget implements Widget { + private final StackPane container; + + public StackWidget(String cssClass) { + container = new StackPane(); + container.getStyleClass().add(cssClass); + } + + public void add(Pos position, Node node) { + Platform.runLater(() -> { + if (container.getChildren().contains(node)) { + return; + } + + StackPane.setAlignment(node, position); + container.getChildren().add(node); + }); + } + + public void add(Pos position, Widget widget) { + add(position, widget.getNode()); + } + + public void remove(Node node) { + Platform.runLater(() -> { + container.getChildren().remove(node); + }); + } + + public void remove(Widget widget) { + remove(widget.getNode()); + } + + @Override + public Node getNode() { + return container; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/ToggleWidget.java b/app/src/main/java/org/toop/app/widget/complex/ToggleWidget.java new file mode 100644 index 0000000..67f338c --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/ToggleWidget.java @@ -0,0 +1,62 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.Widget; +import org.toop.local.AppContext; + +import java.util.function.Consumer; + +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.layout.VBox; + +public class ToggleWidget implements Widget { + private final Button button; + private final VBox container; + + private final String onKey; + private final String offKey; + + private boolean state; + + public ToggleWidget(String onKey, String offKey, boolean initialState, Consumer onToggle) { + this.onKey = onKey; + this.offKey = offKey; + this.state = initialState; + + button = new Button(AppContext.getString(getCurrentKey())); + button.setOnAction(_ -> { + state = !state; + updateText(); + if (onToggle != null) { + onToggle.accept(state); + } + }); + + container = Primitive.vbox(button); + } + + private String getCurrentKey() { + return state? offKey : onKey; + } + + private void updateText() { + button.setText(AppContext.getString(getCurrentKey())); + } + + public boolean getState() { + return state; + } + + public void setState(boolean newState) { + if (state != newState) { + state = newState; + updateText(); + } + } + + @Override + public Node getNode() { + return container; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/TransitionAnimation.java b/app/src/main/java/org/toop/app/widget/complex/TransitionAnimation.java deleted file mode 100644 index 001ff29..0000000 --- a/app/src/main/java/org/toop/app/widget/complex/TransitionAnimation.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.toop.app.widget.complex; - -public interface TransitionAnimation { - void transition(PrimaryWidget primary); -} diff --git a/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java b/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java deleted file mode 100644 index 356af91..0000000 --- a/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.toop.app.widget.complex; - -import javafx.geometry.Pos; -import org.toop.app.widget.Widget; - -import javafx.scene.layout.StackPane; - -public abstract class ViewWidget implements Widget { - private final StackPane container; - - public ViewWidget(String cssClass) { - container = new StackPane(); - container.getStyleClass().add(cssClass); - } - - public void add(Pos position, Widget widget) { - StackPane.setAlignment(widget.getNode(), position); - container.getChildren().add(widget.getNode()); - } - - @Override - public StackPane getNode() { - return container; - } - - public abstract void reload(); -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/display/SongDisplay.java b/app/src/main/java/org/toop/app/widget/display/SongDisplay.java new file mode 100644 index 0000000..c75ce8f --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/display/SongDisplay.java @@ -0,0 +1,117 @@ +package org.toop.app.widget.display; + +import org.toop.app.widget.Widget; +import org.toop.framework.audio.events.AudioEvents; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.eventbus.GlobalEventBus; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ProgressBar; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +public class SongDisplay extends VBox implements Widget { + private final Text songTitle; + private final ProgressBar progressBar; + private final Text progressText; + + public SongDisplay() { + new EventFlow() + .listen(this::updateTheSong); + + setAlignment(Pos.CENTER); + setMaxHeight(Region.USE_PREF_SIZE); + getStyleClass().add("song-display"); + + // TODO ADD GOOD SONG TITLES WITH ARTISTS DISPLAYED + songTitle = new Text("song playing"); + songTitle.getStyleClass().add("song-title"); + + progressBar = new ProgressBar(0); + progressBar.getStyleClass().add("progress-bar"); + + progressText = new Text("0:00/0:00"); + progressText.getStyleClass().add("progress-text"); + + // TODO ADD BETTER CSS FOR THE SKIPBUTTON WHERE ITS AT A NICER POSITION + + Button skipButton = new Button(">>"); + Button pauseButton = new Button("⏸"); + Button previousButton = new Button("<<"); + + skipButton.getStyleClass().setAll("skip-button"); + pauseButton.getStyleClass().setAll("pause-button"); + previousButton.getStyleClass().setAll("previous-button"); + + skipButton.setOnAction( event -> { + GlobalEventBus.post(new AudioEvents.SkipMusic()); + }); + + pauseButton.setOnAction(event -> { + GlobalEventBus.post(new AudioEvents.PauseMusic()); + if (pauseButton.getText().equals("⏸")) { + pauseButton.setText("▶"); + } + else if (pauseButton.getText().equals("▶")) { + pauseButton.setText("⏸"); + } + }); + + previousButton.setOnAction( event -> { + GlobalEventBus.post(new AudioEvents.PreviousMusic()); + }); + + HBox control = new HBox(10, previousButton, pauseButton, skipButton); + control.setAlignment(Pos.CENTER); + control.getStyleClass().add("controls"); + + getChildren().addAll(songTitle, progressBar, progressText, control); + } + + private void updateTheSong(AudioEvents.PlayingMusic event) { + Platform.runLater(() -> { + String text = event.name(); + text = text.substring(0, text.length() - 4); + songTitle.setText(text); + double currentPos = event.currentPosition(); + double duration = event.duration(); + if (currentPos / duration > 0.05) { + double progress = currentPos / duration; + progressBar.setProgress(progress); + } + else if (currentPos / duration < 0.05) { + progressBar.setProgress(0.05); + } + progressText.setText(getTimeString(event.currentPosition(), event.duration())); + }); + } + + private String getTimeString(long position, long duration) { + long positionMinutes = position / 60; + long durationMinutes = duration / 60; + long positionSeconds = position % 60; + long durationSeconds = duration % 60; + String positionSecondsStr = String.valueOf(positionSeconds); + String durationSecondsStr = String.valueOf(durationSeconds); + + if (positionSeconds < 10) { + positionSecondsStr = "0" + positionSeconds; + } + if (durationSeconds < 10) { + durationSecondsStr = "0" + durationSeconds; + } + + String time = positionMinutes + ":" + positionSecondsStr + " / " + durationMinutes + ":" + durationSecondsStr; + return time; + } + + @Override + public Node getNode() { + return this; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/popup/QuitPopup.java b/app/src/main/java/org/toop/app/widget/popup/QuitPopup.java new file mode 100644 index 0000000..04349dc --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/popup/QuitPopup.java @@ -0,0 +1,24 @@ +package org.toop.app.widget.popup; + +import org.toop.app.App; +import org.toop.app.widget.complex.ConfirmWidget; +import org.toop.app.widget.complex.PopupWidget; + +import javafx.geometry.Pos; + +public class QuitPopup extends PopupWidget { + public QuitPopup() { + var confirmWidget = new ConfirmWidget("are-you-sure"); + + confirmWidget.addButton("yes", () -> { + App.quit(); + }); + + confirmWidget.addButton("no", () -> { + App.stopQuit(); + hide(); + }); + + add(Pos.CENTER, confirmWidget); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/primary/CreditsPrimary.java b/app/src/main/java/org/toop/app/widget/primary/CreditsPrimary.java new file mode 100644 index 0000000..82590cb --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/primary/CreditsPrimary.java @@ -0,0 +1,81 @@ +package org.toop.app.widget.primary; + +import org.toop.app.App; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.PrimaryWidget; + +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.geometry.Pos; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.text.Text; +import javafx.util.Duration; + +public class CreditsPrimary extends PrimaryWidget { + public CreditsPrimary() { + var scrumMasterCredit = newCredit("scrum-master", "Stef"); + var productOwnerCredit = newCredit("product-owner", "Omar"); + var mergeCommanderCredit = newCredit("merge-commander", "Bas"); + var localizationCredit = newCredit("localization", "Ticho"); + var aiCredit = newCredit("ai", "Michiel"); + var developersCredit = newCredit("developers", "Michiel, Bas, Stef, Omar, Ticho"); + var moralSupportCredit = newCredit("moral-support", "Wesley"); + var openglCredit = newCredit("opengl", "Omar"); + + var topSpacer = new Region(); + topSpacer.setPrefHeight(App.getHeight()); + + var bottomSpacer = new Region(); + bottomSpacer.setPrefHeight(App.getHeight()); + + var creditsContainer = Primitive.vbox( + topSpacer, + + scrumMasterCredit, + productOwnerCredit, + mergeCommanderCredit, + localizationCredit, + aiCredit, + developersCredit, + moralSupportCredit, + openglCredit, + + bottomSpacer + ); + + var creditsScroll = Primitive.scroll(creditsContainer); + + creditsScroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + creditsScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + + add(Pos.CENTER, creditsScroll); + + animate(creditsScroll, 15); + } + + private HBox newCredit(String key, String other) { + var credit = new Text(": " + other); + credit.getStyleClass().add("header"); + + var creditBox = Primitive.hbox( + Primitive.header(key), + credit + ); + + creditBox.setPrefHeight(App.getHeight() / 3.0); + return creditBox; + } + + private void animate(ScrollPane scroll, int length) { + final Timeline timeline = new Timeline( + new KeyFrame(Duration.seconds(0), new KeyValue(scroll.vvalueProperty(), 0.0)), + new KeyFrame(Duration.seconds(length), new KeyValue(scroll.vvalueProperty(), 1.0)) + ); + + timeline.setCycleCount(Timeline.INDEFINITE); + timeline.play(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/primary/LocalPrimary.java b/app/src/main/java/org/toop/app/widget/primary/LocalPrimary.java new file mode 100644 index 0000000..ef0364f --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/primary/LocalPrimary.java @@ -0,0 +1,25 @@ +package org.toop.app.widget.primary; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.PrimaryWidget; + +import javafx.geometry.Pos; + +public class LocalPrimary extends PrimaryWidget { + public LocalPrimary() { + var ticTacToeButton = Primitive.button("tic-tac-toe", () -> { + }); + + var reversiButton = Primitive.button("reversi", () -> { + }); + + var connect4Button = Primitive.button("connect4", () -> { + }); + + add(Pos.CENTER, Primitive.vbox( + ticTacToeButton, + reversiButton, + connect4Button + )); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/primary/MainPrimary.java b/app/src/main/java/org/toop/app/widget/primary/MainPrimary.java new file mode 100644 index 0000000..06ecdf0 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/primary/MainPrimary.java @@ -0,0 +1,39 @@ +package org.toop.app.widget.primary; + +import org.toop.app.App; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.PrimaryWidget; + +import javafx.geometry.Pos; + +public class MainPrimary extends PrimaryWidget { + public MainPrimary() { + var localButton = Primitive.button("local", () -> { + transitionNext(new LocalPrimary()); + }); + + var onlineButton = Primitive.button("online", () -> { + transitionNext(new OnlinePrimary()); + }); + + var creditsButton = Primitive.button("credits", () -> { + transitionNext(new CreditsPrimary()); + }); + + var optionsButton = Primitive.button("options", () -> { + transitionNext(new OptionsPrimary()); + }); + + var quitButton = Primitive.button("quit", () -> { + App.startQuit(); + }); + + add(Pos.CENTER, Primitive.vbox( + localButton, + onlineButton, + creditsButton, + optionsButton, + quitButton + )); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/primary/OnlinePrimary.java b/app/src/main/java/org/toop/app/widget/primary/OnlinePrimary.java new file mode 100644 index 0000000..98bc68e --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/primary/OnlinePrimary.java @@ -0,0 +1,38 @@ +package org.toop.app.widget.primary; + +import org.toop.app.Server; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.LabeledInputWidget; +import org.toop.app.widget.complex.PrimaryWidget; + +import javafx.geometry.Pos; + +public class OnlinePrimary extends PrimaryWidget { + public OnlinePrimary() { + var serverInformationHeader = Primitive.header("server-information"); + + var serverIPInput = new LabeledInputWidget("ip-address", "enter-the-server-ip", "", _ -> {}); + var serverPortInput = new LabeledInputWidget("port", "enter-the-server-port", "", _ -> {}); + var playerNameInput = new LabeledInputWidget("player-name", "enter-your-name", "", _ -> {}); + + var connectButton = Primitive.button("connect", () -> { + new Server( + serverIPInput.getValue(), + serverPortInput.getValue(), + playerNameInput.getValue() + ); + }); + + add(Pos.CENTER, Primitive.vbox( + serverInformationHeader, + Primitive.separator(), + + serverIPInput.getNode(), + serverPortInput.getNode(), + playerNameInput.getNode(), + Primitive.separator(), + + connectButton + )); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/primary/OptionsPrimary.java b/app/src/main/java/org/toop/app/widget/primary/OptionsPrimary.java new file mode 100644 index 0000000..9061df4 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/primary/OptionsPrimary.java @@ -0,0 +1,161 @@ +package org.toop.app.widget.primary; + +import org.toop.app.App; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.LabeledChoiceWidget; +import org.toop.app.widget.complex.LabeledSliderWidget; +import org.toop.app.widget.complex.PrimaryWidget; +import org.toop.app.widget.complex.ToggleWidget; +import org.toop.framework.audio.VolumeControl; +import org.toop.framework.audio.events.AudioEvents; +import org.toop.framework.eventbus.EventFlow; +import org.toop.local.AppContext; +import org.toop.local.AppSettings; + +import java.util.Locale; + +import javafx.geometry.Pos; +import javafx.scene.layout.VBox; +import javafx.util.StringConverter; + +public class OptionsPrimary extends PrimaryWidget { + public OptionsPrimary() { + add(Pos.CENTER, Primitive.hbox( + generalSection(), + volumeSection(), + styleSection() + )); + } + + private VBox generalSection() { + var languageWidget = new LabeledChoiceWidget<>( + "language", + new StringConverter<>() { + @Override + public String toString(Locale locale) { + return AppContext.getString(locale.getDisplayName().toLowerCase()); + } + @Override + public Locale fromString(String s) { return null; } + }, + AppContext.getLocale(), + newLocale -> { + AppSettings.getSettings().setLocale(newLocale.toString()); + AppContext.setLocale(newLocale); + reload(new OptionsPrimary()); + }, + AppContext.getLocalization().getAvailableLocales().toArray(new Locale[0]) + ); + + var fullscreenToggle = new ToggleWidget( + "fullscreen", "windowed", + AppSettings.getSettings().getFullscreen(), + fullscreen -> { + AppSettings.getSettings().setFullscreen(fullscreen); + App.setFullscreen(fullscreen); + } + ); + + return Primitive.vbox( + Primitive.header("general"), + Primitive.separator(), + + languageWidget.getNode(), + fullscreenToggle.getNode() + ); + } + + private VBox volumeSection() { + var masterVolumeWidget = new LabeledSliderWidget( + "master-volume", + 0, 100, + AppSettings.getSettings().getVolume(), + val -> { + AppSettings.getSettings().setVolume(val); + new EventFlow() + .addPostEvent(new AudioEvents.ChangeVolume(val, VolumeControl.MASTERVOLUME)) + .asyncPostEvent(); + } + ); + + var effectsVolumeWidget = new LabeledSliderWidget( + "effects-volume", + 0, 100, + AppSettings.getSettings().getFxVolume(), + val -> { + AppSettings.getSettings().setFxVolume(val); + new EventFlow() + .addPostEvent(new AudioEvents.ChangeVolume(val, VolumeControl.FX)) + .asyncPostEvent(); + } + ); + + var musicVolumeWidget = new LabeledSliderWidget( + "music-volume", + 0, 100, + AppSettings.getSettings().getMusicVolume(), + val -> { + AppSettings.getSettings().setMusicVolume(val); + new EventFlow() + .addPostEvent(new AudioEvents.ChangeVolume(val, VolumeControl.MUSIC)) + .asyncPostEvent(); + } + ); + + return Primitive.vbox( + Primitive.header("volume"), + Primitive.separator(), + + masterVolumeWidget.getNode(), + effectsVolumeWidget.getNode(), + musicVolumeWidget.getNode() + ); + } + + private VBox styleSection() { + var themeWidget = new LabeledChoiceWidget<>( + "theme", + new StringConverter<>() { + @Override + public String toString(String theme) { + return AppContext.getString(theme); + } + @Override + public String fromString(String s) { return null; } + }, + AppSettings.getSettings().getTheme(), + newTheme -> { + AppSettings.getSettings().setTheme(newTheme); + App.setStyle(newTheme, AppSettings.getSettings().getLayoutSize()); + }, + "dark", "light", "high-contrast" + ); + + var layoutWidget = new LabeledChoiceWidget<>( + "layout-size", + new StringConverter<>() { + @Override + public String toString(String layout) { + return AppContext.getString(layout); + } + @Override + public String fromString(String s) { return null; } + }, + AppSettings.getSettings().getLayoutSize(), + newLayout -> { + AppSettings.getSettings().setLayoutSize(newLayout); + App.setStyle(AppSettings.getSettings().getTheme(), newLayout); + }, + "small", "medium", "large" + ); + + + return Primitive.vbox( + Primitive.header("style"), + Primitive.separator(), + + themeWidget.getNode(), + layoutWidget.getNode() + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/simple/LabeledButtonWidget.java b/app/src/main/java/org/toop/app/widget/simple/LabeledButtonWidget.java deleted file mode 100644 index 8436f95..0000000 --- a/app/src/main/java/org/toop/app/widget/simple/LabeledButtonWidget.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.toop.app.widget.simple; - -import org.toop.app.widget.Widget; - -import javafx.scene.control.Button; -import javafx.scene.layout.VBox; -import javafx.scene.text.Text; - -public class LabeledButtonWidget extends VBox implements Widget { - public LabeledButtonWidget( - String labelText, - String buttonText, Runnable buttonOnAction - ) { - var text = new Text(labelText); - - var button = new Button(buttonText); - button.setOnAction(_ -> buttonOnAction.run()); - - super(text, button); - } - - @Override - public VBox getNode() { - return this; - } -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/view/MainView.java b/app/src/main/java/org/toop/app/widget/view/MainView.java deleted file mode 100644 index 1d77b52..0000000 --- a/app/src/main/java/org/toop/app/widget/view/MainView.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.toop.app.widget.view; - -public class MainView { -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/view/QuitView.java b/app/src/main/java/org/toop/app/widget/view/QuitView.java deleted file mode 100644 index bbd977c..0000000 --- a/app/src/main/java/org/toop/app/widget/view/QuitView.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.toop.app.widget.view; - -import org.toop.app.widget.complex.PopupWidget; - -public class QuitView extends PopupWidget { - protected QuitView() { - } - - @Override - public void reload() { - } -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/local/AppContext.java b/app/src/main/java/org/toop/local/AppContext.java index 4d96fb4..84326e0 100644 --- a/app/src/main/java/org/toop/local/AppContext.java +++ b/app/src/main/java/org/toop/local/AppContext.java @@ -1,19 +1,28 @@ package org.toop.local; -import java.util.Locale; import org.toop.framework.resource.ResourceManager; import org.toop.framework.resource.resources.LocalizationAsset; +import java.util.Locale; + +import javafx.beans.binding.Bindings; +import javafx.beans.binding.StringBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + public class AppContext { private static final LocalizationAsset localization = ResourceManager.get("localization"); private static Locale locale = Locale.forLanguageTag("en"); + private static final ObjectProperty localeProperty = new SimpleObjectProperty<>(locale); + public static LocalizationAsset getLocalization() { return localization; } public static void setLocale(Locale locale) { AppContext.locale = locale; + localeProperty.set(locale); } public static Locale getLocale() { @@ -21,7 +30,13 @@ public class AppContext { } public static String getString(String key) { - assert localization != null; return localization.getString(key, locale); } -} + + public static StringBinding bindToKey(String key) { + return Bindings.createStringBinding( + () -> localization.getString(key, locale), + localeProperty + ); + } +} \ No newline at end of file diff --git a/app/src/main/resources/assets/style/general.css b/app/src/main/resources/assets/style/general.css index deeda43..69c5ec4 100644 --- a/app/src/main/resources/assets/style/general.css +++ b/app/src/main/resources/assets/style/general.css @@ -3,36 +3,35 @@ -fx-padding: 0; } -.container, -.credits-container { +.container { -fx-alignment: TOP_CENTER; -fx-background-color: transparent; } -.fit, -.fit .viewport { +.scroll, +.scroll .viewport { -fx-background-color: transparent; -fx-border-color: transparent; } -.fit .scroll-bar .decrement-arrow, -.fit .scroll-bar .decrement-button, -.fit .scroll-bar .increment-arrow, -.fit .scroll-bar .increment-button { +.scroll .scroll-bar .decrement-arrow, +.scroll .scroll-bar .decrement-button, +.scroll .scroll-bar .increment-arrow, +.scroll .scroll-bar .increment-button { -fx-padding: 0; -fx-pref-height: 0; -fx-pref-width: 0; -fx-shape: ""; } -.fit .scroll-bar .thumb { +.scroll .scroll-bar .thumb { -fx-background-color: #888; -fx-background-insets: 0; -fx-background-radius: 1px; } -.fit .scroll-bar:horizontal, -.fit .scroll-bar:vertical { +.scroll .scroll-bar:horizontal, +.scroll .scroll-bar:vertical { -fx-pref-height: 4px; -fx-pref-width: 4px; } From fa4e1ad5e379d3c61b9bbd7ca3d2fa18139c2990 Mon Sep 17 00:00:00 2001 From: ramollia <> Date: Mon, 3 Nov 2025 12:47:56 +0100 Subject: [PATCH 5/7] widget system almost complete --- app/src/main/java/org/toop/app/App.java | 5 +- app/src/main/java/org/toop/app/Server.java | 79 ++------ .../org/toop/app/canvas/ReversiCanvas.java | 3 +- .../org/toop/app/game/BaseGameThread.java | 120 ++++++++++++ .../java/org/toop/app/game/ReversiGame.java | 52 +++-- .../java/org/toop/app/game/TicTacToeGame.java | 44 ++--- .../toop/app/game/TicTacToeGameThread.java | 177 ++++++++++++++++++ .../app/view/views/LocalMultiplayerView.java | 6 +- .../java/org/toop/app/widget/Primitive.java | 36 +++- .../org/toop/app/widget/WidgetContainer.java | 37 ++-- .../app/widget/complex/ConfirmWidget.java | 9 +- .../app/widget/complex/PlayerInfoWidget.java | 83 ++++++++ .../app/widget/complex/PrimaryWidget.java | 43 ----- .../toop/app/widget/complex/ViewWidget.java | 49 +++++ .../toop/app/widget/popup/ChallengePopup.java | 60 ++++++ .../org/toop/app/widget/popup/ErrorPopup.java | 16 ++ .../toop/app/widget/popup/GameOverPopup.java | 25 +++ .../app/widget/popup/SendChallengePopup.java | 90 +++++++++ .../toop/app/widget/primary/LocalPrimary.java | 25 --- .../CreditsView.java} | 8 +- .../org/toop/app/widget/view/GameView.java | 96 ++++++++++ .../app/widget/view/LocalMultiplayerView.java | 74 ++++++++ .../org/toop/app/widget/view/LocalView.java | 29 +++ .../MainPrimary.java => view/MainView.java} | 16 +- .../OnlineView.java} | 8 +- .../OptionsView.java} | 10 +- .../org/toop/app/widget/view/ServerView.java | 60 ++++++ 27 files changed, 1014 insertions(+), 246 deletions(-) create mode 100644 app/src/main/java/org/toop/app/game/BaseGameThread.java create mode 100644 app/src/main/java/org/toop/app/game/TicTacToeGameThread.java create mode 100644 app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java delete mode 100644 app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java create mode 100644 app/src/main/java/org/toop/app/widget/complex/ViewWidget.java create mode 100644 app/src/main/java/org/toop/app/widget/popup/ChallengePopup.java create mode 100644 app/src/main/java/org/toop/app/widget/popup/ErrorPopup.java create mode 100644 app/src/main/java/org/toop/app/widget/popup/GameOverPopup.java create mode 100644 app/src/main/java/org/toop/app/widget/popup/SendChallengePopup.java delete mode 100644 app/src/main/java/org/toop/app/widget/primary/LocalPrimary.java rename app/src/main/java/org/toop/app/widget/{primary/CreditsPrimary.java => view/CreditsView.java} (92%) create mode 100644 app/src/main/java/org/toop/app/widget/view/GameView.java create mode 100644 app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java create mode 100644 app/src/main/java/org/toop/app/widget/view/LocalView.java rename app/src/main/java/org/toop/app/widget/{primary/MainPrimary.java => view/MainView.java} (63%) rename app/src/main/java/org/toop/app/widget/{primary/OnlinePrimary.java => view/OnlineView.java} (84%) rename app/src/main/java/org/toop/app/widget/{primary/OptionsPrimary.java => view/OptionsView.java} (95%) create mode 100644 app/src/main/java/org/toop/app/widget/view/ServerView.java diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index db6ec38..88cd949 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -1,10 +1,9 @@ package org.toop.app; -import org.toop.app.widget.Widget; import org.toop.app.widget.WidgetContainer; import org.toop.app.widget.display.SongDisplay; import org.toop.app.widget.popup.QuitPopup; -import org.toop.app.widget.primary.MainPrimary; +import org.toop.app.widget.view.MainView; import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.resource.ResourceManager; @@ -65,7 +64,7 @@ public final class App extends Application { AppSettings.applySettings(); new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent(); - WidgetContainer.add(Pos.CENTER, new MainPrimary()); + WidgetContainer.add(Pos.CENTER, new MainView()); WidgetContainer.add(Pos.BOTTOM_RIGHT, new SongDisplay()); } diff --git a/app/src/main/java/org/toop/app/Server.java b/app/src/main/java/org/toop/app/Server.java index 9421004..00003b9 100644 --- a/app/src/main/java/org/toop/app/Server.java +++ b/app/src/main/java/org/toop/app/Server.java @@ -3,12 +3,11 @@ package org.toop.app; import org.toop.app.game.Connect4Game; import org.toop.app.game.ReversiGame; import org.toop.app.game.TicTacToeGame; -import org.toop.app.view.ViewStack; -import org.toop.app.view.views.ChallengeView; -import org.toop.app.view.views.ErrorView; -import org.toop.app.view.views.OnlineView; -import org.toop.app.view.views.SendChallengeView; -import org.toop.app.view.views.ServerView; +import org.toop.app.widget.WidgetContainer; +import org.toop.app.widget.popup.ChallengePopup; +import org.toop.app.widget.popup.ErrorPopup; +import org.toop.app.widget.popup.SendChallengePopup; +import org.toop.app.widget.view.ServerView; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.networking.clients.TournamentNetworkingClient; import org.toop.framework.networking.events.NetworkEvents; @@ -29,10 +28,10 @@ public final class Server { private final List onlinePlayers = new CopyOnWriteArrayList<>(); private final List gameList = new CopyOnWriteArrayList<>(); - private ServerView view; + private ServerView primary; private boolean isPolling = true; - private AtomicBoolean isSingleGame = new AtomicBoolean(false); + private final AtomicBoolean isSingleGame = new AtomicBoolean(false); private ScheduledExecutorService scheduler; @@ -52,7 +51,7 @@ public final class Server { public Server(String ip, String port, String user) { if (ip.split("\\.").length < 4) { - ViewStack.push(new ErrorView("\"" + ip + "\" " + AppContext.getString("is-not-a-valid-ip-address"))); + new ErrorPopup("\"" + ip + "\" " + AppContext.getString("is-not-a-valid-ip-address")); return; } @@ -61,12 +60,12 @@ public final class Server { try { parsedPort = Integer.parseInt(port); } catch (NumberFormatException _) { - ViewStack.push(new ErrorView("\"" + port + "\" " + AppContext.getString("is-not-a-valid-port"))); + new ErrorPopup("\"" + port + "\" " + AppContext.getString("is-not-a-valid-port")); return; } if (user.isEmpty() || user.matches("^[0-9].*")) { - ViewStack.push(new ErrorView(AppContext.getString("invalid-username"))); + new ErrorPopup(AppContext.getString("invalid-username")); return; } @@ -81,8 +80,8 @@ public final class Server { new EventFlow().addPostEvent(new NetworkEvents.SendLogin(clientId, user)).postEvent(); - view = new ServerView(user, this::sendChallenge, this::disconnect); - ViewStack.push(view); + primary = new ServerView(user, this::sendChallenge, this::disconnect); + WidgetContainer.getCurrentView().transitionNext(primary); startPopulateScheduler(); populateGameList(); @@ -96,38 +95,10 @@ public final class Server { private void sendChallenge(String opponent) { if (!isPolling) return; - - ViewStack.push(new SendChallengeView(this, opponent, (playerInformation, gameType) -> { + new SendChallengePopup(this, opponent, (playerInformation, gameType) -> { new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType)).postEvent(); - /* .listen(NetworkEvents.GameMatchResponse.class, e -> { - if (e.clientId() == clientId) { - isPolling = false; - onlinePlayers.clear(); - - final GameInformation.Type type = gameToType(gameType); - if (type == null) { - ViewStack.push(new ErrorView("Unsupported game type: " + gameType)); - return; - } - - final int myTurn = e.playerToMove().equalsIgnoreCase(e.opponent()) ? 1 : 0; - - final GameInformation information = new GameInformation(type); - information.players[0] = playerInformation; - information.players[0].name = user; - information.players[1].name = opponent; - - switch (type) { - case TICTACTOE -> new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); - case REVERSI -> new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); - case CONNECT4 -> new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); - default -> ViewStack.push(new ErrorView("Unsupported game type.")); - } - } - }) */ - ViewStack.pop(); isSingleGame.set(true); - })); + }); } private void handleMatchResponse(NetworkEvents.GameMatchResponse response) { @@ -141,7 +112,7 @@ public final class Server { final GameInformation.Type type = gameToType(gameType); if (type == null) { - ViewStack.push(new ErrorView("Unsupported game type: " + gameType)); + new ErrorPopup("Unsupported game type: " + gameType); return; } @@ -164,7 +135,7 @@ public final class Server { new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); case CONNECT4 -> new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); - default -> ViewStack.push(new ErrorView("Unsupported game type.")); + default -> new ErrorPopup("Unsupported game type."); } } } @@ -175,17 +146,11 @@ public final class Server { String challengerName = extractQuotedValue(response.challengerName()); String gameType = extractQuotedValue(response.gameType()); final String finalGameType = gameType; - ViewStack.push(new ChallengeView(challengerName, gameType, (playerInformation) -> { + new ChallengePopup(challengerName, gameType, (playerInformation) -> { final int challengeId = Integer.parseInt(response.challengeId().replaceAll("\\D", "")); new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientId, challengeId)).postEvent(); - ViewStack.pop(); isSingleGame.set(true); - - //new EventFlow().listen(NetworkEvents.GameMatchResponse.class, e -> { - - - //}); - })); + }); } private void sendMessage(String message) { @@ -196,7 +161,7 @@ public final class Server { new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent(); isPolling = false; stopScheduler(); - ViewStack.push(new OnlineView()); + primary.transitionPrevious(); } private void forfeitGame() { @@ -205,15 +170,11 @@ public final class Server { private void exitGame() { forfeitGame(); - ViewStack.push(view); startPopulateScheduler(); } private void gameOver(){ - ViewStack.pop(); - ViewStack.push(view); startPopulateScheduler(); - } private void startPopulateScheduler() { @@ -227,7 +188,7 @@ public final class Server { onlinePlayers.clear(); onlinePlayers.addAll(List.of(e.playerlist())); onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user)); - view.update(onlinePlayers); + primary.update(onlinePlayers); } }, false); diff --git a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java index 313e0e9..9b9cecf 100644 --- a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java @@ -18,7 +18,6 @@ public final class ReversiCanvas extends GameCanvas { } public void drawLegalPosition(Color color, int cell) { - fill(new Color(color.getRed() * 0.25, color.getGreen() * 0.25, color.getBlue() * 0.25, 1.0), cell); - drawDot(new Color(color.getRed() * 0.5, color.getGreen() * 0.5, color.getBlue() * 0.5, 1.0), cell); + drawDot(new Color(color.getRed(), color.getGreen(), color.getBlue(), 0.25), cell); } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/game/BaseGameThread.java b/app/src/main/java/org/toop/app/game/BaseGameThread.java new file mode 100644 index 0000000..f9178da --- /dev/null +++ b/app/src/main/java/org/toop/app/game/BaseGameThread.java @@ -0,0 +1,120 @@ +package org.toop.app.game; + +import org.toop.app.GameInformation; +import org.toop.app.widget.WidgetContainer; +import org.toop.app.widget.view.GameView; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.game.Game; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public abstract class BaseGameThread { + protected final GameInformation information; + protected final int myTurn; + protected final Runnable onGameOver; + protected final BlockingQueue moveQueue; + + protected final TGame game; + protected final TAI ai; + + protected final GameView primary; + protected final TCanvas canvas; + + protected final AtomicBoolean isRunning = new AtomicBoolean(true); + + protected BaseGameThread( + GameInformation information, + int myTurn, + Runnable onForfeit, + Runnable onExit, + Consumer onMessage, + Runnable onGameOver, + Supplier gameSupplier, + Supplier aiSupplier, + Function, TCanvas> canvasFactory) { + + this.information = information; + this.myTurn = myTurn; + this.onGameOver = onGameOver; + this.moveQueue = new LinkedBlockingQueue<>(); + + this.game = gameSupplier.get(); + this.ai = aiSupplier.get(); + + if (onForfeit == null || onExit == null) { + primary = new GameView(null, () -> { + isRunning.set(false); + WidgetContainer.getCurrentView().transitionPrevious(); + }, null); + } else { + primary = new GameView(onForfeit, () -> { + isRunning.set(false); + onExit.run(); + }, onMessage); + } + + this.canvas = canvasFactory.apply(this::onCellClicked); + + addCanvasToPrimary(); + + WidgetContainer.getCurrentView().transitionNext(primary); + + if (onForfeit == null || onExit == null) + new Thread(this::localGameThread).start(); + else + new EventFlow() + .listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse) + .listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse); + + setGameLabels(myTurn == 0); + } + + private void onCellClicked(int cell) { + if (!isRunning.get()) return; + + final int currentTurn = getCurrentTurn(); + if (!information.players[currentTurn].isHuman) return; + + final char value = getSymbolForTurn(currentTurn); + + try { + moveQueue.put(new Game.Move(cell, value)); + } catch (InterruptedException _) {} + } + + protected void gameOver() { + if (onGameOver != null) { + isRunning.set(false); + onGameOver.run(); + } + } + + protected void setGameLabels(boolean isMe) { + final int currentTurn = getCurrentTurn(); + final String turnName = getNameForTurn(currentTurn); + + primary.nextPlayer( + isMe, + information.players[isMe ? 0 : 1].name, + turnName, + information.players[isMe ? 1 : 0].name + ); + } + + protected abstract void addCanvasToPrimary(); + + protected abstract int getCurrentTurn(); + protected abstract char getSymbolForTurn(int turn); + protected abstract String getNameForTurn(int turn); + + protected abstract void onMoveResponse(NetworkEvents.GameMoveResponse response); + protected abstract void onYourTurnResponse(NetworkEvents.YourTurnResponse response); + + protected abstract void localGameThread(); +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/game/ReversiGame.java b/app/src/main/java/org/toop/app/game/ReversiGame.java index 3645fb0..36afd09 100644 --- a/app/src/main/java/org/toop/app/game/ReversiGame.java +++ b/app/src/main/java/org/toop/app/game/ReversiGame.java @@ -4,9 +4,8 @@ import javafx.animation.SequentialTransition; import org.toop.app.App; import org.toop.app.GameInformation; import org.toop.app.canvas.ReversiCanvas; -import org.toop.app.view.ViewStack; -import org.toop.app.view.views.GameView; -import org.toop.app.view.views.LocalMultiplayerView; +import org.toop.app.widget.WidgetContainer; +import org.toop.app.widget.view.GameView; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.networking.events.NetworkEvents; import org.toop.game.Game; @@ -25,13 +24,13 @@ public final class ReversiGame { private final GameInformation information; private final int myTurn; - private Runnable onGameOver; + private final Runnable onGameOver; private final BlockingQueue moveQueue; private final Reversi game; private final ReversiAI ai; - private final GameView view; + private final GameView primary; private final ReversiCanvas canvas; private final AtomicBoolean isRunning; @@ -42,7 +41,7 @@ public final class ReversiGame { this.myTurn = myTurn; this.onGameOver = onGameOver; - moveQueue = new LinkedBlockingQueue(); + moveQueue = new LinkedBlockingQueue<>(); game = new Reversi(); ai = new ReversiAI(); @@ -51,12 +50,12 @@ public final class ReversiGame { isPaused = new AtomicBoolean(false); if (onForfeit == null || onExit == null) { - view = new GameView(null, () -> { + primary = new GameView(null, () -> { isRunning.set(false); - ViewStack.push(new LocalMultiplayerView(information)); + WidgetContainer.getCurrentView().transitionPrevious(); }, null); } else { - view = new GameView(onForfeit, () -> { + primary = new GameView(onForfeit, () -> { isRunning.set(false); onExit.run(); }, onMessage); @@ -84,8 +83,8 @@ public final class ReversiGame { } }); - view.add(Pos.CENTER, canvas.getCanvas()); - ViewStack.push(view); + primary.add(Pos.CENTER, canvas.getCanvas()); + WidgetContainer.getCurrentView().transitionNext(primary); if (onForfeit == null || onExit == null) { new Thread(this::localGameThread).start(); @@ -93,8 +92,7 @@ public final class ReversiGame { } else { new EventFlow() .listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse) - .listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse) - .listen(NetworkEvents.ReceivedMessage.class, this::onReceivedMessage); + .listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse); setGameLabels(myTurn == 0); } @@ -120,7 +118,7 @@ public final class ReversiGame { final String currentValue = currentTurn == 0? "BLACK" : "WHITE"; final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); - view.nextPlayer(information.players[currentTurn].isHuman, + primary.nextPlayer(information.players[currentTurn].isHuman, information.players[currentTurn].name, currentValue, information.players[nextTurn].name); @@ -164,9 +162,9 @@ public final class ReversiGame { if (state != Game.State.NORMAL) { if (state == Game.State.WIN) { - view.gameOver(true, information.players[currentTurn].name); + primary.gameOver(true, information.players[currentTurn].name); } else if (state == Game.State.DRAW) { - view.gameOver(false, ""); + primary.gameOver(false, ""); } isRunning.set(false); @@ -193,14 +191,14 @@ public final class ReversiGame { if (state != Game.State.NORMAL) { if (state == Game.State.WIN) { if (response.player().equalsIgnoreCase(information.players[0].name)) { - view.gameOver(true, information.players[0].name); + primary.gameOver(true, information.players[0].name); gameOver(); } else { - view.gameOver(false, information.players[1].name); + primary.gameOver(false, information.players[1].name); gameOver(); } } else if (state == Game.State.DRAW) { - view.gameOver(false, ""); + primary.gameOver(false, ""); game.play(move); } } @@ -241,14 +239,6 @@ public final class ReversiGame { .postEvent(); } - private void onReceivedMessage(NetworkEvents.ReceivedMessage msg) { - if (!isRunning.get()) { - return; - } - - view.updateChat(msg.message()); - } - private void updateCanvas(boolean animate) { // Todo: this is very inefficient. still very fast but if the grid is bigger it might cause issues. improve. canvas.clearAll(); @@ -266,8 +256,8 @@ public final class ReversiGame { final SequentialTransition animation = new SequentialTransition(); isPaused.set(true); - final Color fromColor = game.getCurrentPlayer() == 0? Color.WHITE : Color.BLACK; - final Color toColor = game.getCurrentPlayer() == 0? Color.BLACK : Color.WHITE; + final Color fromColor = game.getCurrentPlayer() == 'W'? Color.WHITE : Color.BLACK; + final Color toColor = game.getCurrentPlayer() == 'W'? Color.BLACK : Color.WHITE; if (animate && flipped != null) { for (final Game.Move flip : flipped) { @@ -283,7 +273,7 @@ public final class ReversiGame { final Game.Move[] legalMoves = game.getLegalMoves(); for (final Game.Move legalMove : legalMoves) { - canvas.drawLegalPosition(toColor, legalMove.position()); + canvas.drawLegalPosition(fromColor, legalMove.position()); } }); @@ -294,7 +284,7 @@ public final class ReversiGame { final int currentTurn = game.getCurrentTurn(); final String currentValue = currentTurn == 0? "BLACK" : "WHITE"; - view.nextPlayer(isMe, + primary.nextPlayer(isMe, information.players[isMe? 0 : 1].name, currentValue, information.players[isMe? 1 : 0].name); diff --git a/app/src/main/java/org/toop/app/game/TicTacToeGame.java b/app/src/main/java/org/toop/app/game/TicTacToeGame.java index 1ca98a9..12d800f 100644 --- a/app/src/main/java/org/toop/app/game/TicTacToeGame.java +++ b/app/src/main/java/org/toop/app/game/TicTacToeGame.java @@ -3,9 +3,8 @@ package org.toop.app.game; import org.toop.app.App; import org.toop.app.GameInformation; import org.toop.app.canvas.TicTacToeCanvas; -import org.toop.app.view.ViewStack; -import org.toop.app.view.views.GameView; -import org.toop.app.view.views.LocalMultiplayerView; +import org.toop.app.widget.WidgetContainer; +import org.toop.app.widget.view.GameView; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.networking.events.NetworkEvents; import org.toop.game.Game; @@ -24,13 +23,13 @@ public final class TicTacToeGame { private final GameInformation information; private final int myTurn; - private Runnable onGameOver; + private final Runnable onGameOver; private final BlockingQueue moveQueue; private final TicTacToe game; private final TicTacToeAI ai; - private final GameView view; + private final GameView primary; private final TicTacToeCanvas canvas; private final AtomicBoolean isRunning; @@ -48,12 +47,12 @@ public final class TicTacToeGame { isRunning = new AtomicBoolean(true); if (onForfeit == null || onExit == null) { - view = new GameView(null, () -> { + primary = new GameView(null, () -> { isRunning.set(false); - ViewStack.push(new LocalMultiplayerView(information)); + WidgetContainer.getCurrentView().transitionPrevious(); }, null); } else { - view = new GameView(onForfeit, () -> { + primary = new GameView(onForfeit, () -> { isRunning.set(false); onExit.run(); }, onMessage); @@ -81,16 +80,15 @@ public final class TicTacToeGame { } }); - view.add(Pos.CENTER, canvas.getCanvas()); - ViewStack.push(view); + primary.add(Pos.CENTER, canvas.getCanvas()); + WidgetContainer.getCurrentView().transitionNext(primary); if (onForfeit == null || onExit == null) { new Thread(this::localGameThread).start(); } else { new EventFlow() .listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse) - .listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse) - .listen(NetworkEvents.ReceivedMessage.class, this::onReceivedMessage); + .listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse); setGameLabels(myTurn == 0); } @@ -106,7 +104,7 @@ public final class TicTacToeGame { final String currentValue = currentTurn == 0? "X" : "O"; final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); - view.nextPlayer(information.players[currentTurn].isHuman, + primary.nextPlayer(information.players[currentTurn].isHuman, information.players[currentTurn].name, currentValue, information.players[nextTurn].name); @@ -155,9 +153,9 @@ public final class TicTacToeGame { if (state != Game.State.NORMAL) { if (state == Game.State.WIN) { - view.gameOver(true, information.players[currentTurn].name); + primary.gameOver(true, information.players[currentTurn].name); } else if (state == Game.State.DRAW) { - view.gameOver(false, ""); + primary.gameOver(false, ""); } isRunning.set(false); @@ -184,15 +182,15 @@ public final class TicTacToeGame { if (state != Game.State.NORMAL) { if (state == Game.State.WIN) { if (response.player().equalsIgnoreCase(information.players[0].name)) { - view.gameOver(true, information.players[0].name); + primary.gameOver(true, information.players[0].name); gameOver(); } else { - view.gameOver(false, information.players[1].name); + primary.gameOver(false, information.players[1].name); gameOver(); } } else if (state == Game.State.DRAW) { if(game.getLegalMoves().length == 0) { //only return draw in online multiplayer if the game is actually over. - view.gameOver(false, ""); + primary.gameOver(false, ""); gameOver(); } } @@ -244,19 +242,11 @@ public final class TicTacToeGame { .postEvent(); } - private void onReceivedMessage(NetworkEvents.ReceivedMessage msg) { - if (!isRunning.get()) { - return; - } - - view.updateChat(msg.message()); - } - private void setGameLabels(boolean isMe) { final int currentTurn = game.getCurrentTurn(); final String currentValue = currentTurn == 0? "X" : "O"; - view.nextPlayer(isMe, + primary.nextPlayer(isMe, information.players[isMe? 0 : 1].name, currentValue, information.players[isMe? 1 : 0].name); diff --git a/app/src/main/java/org/toop/app/game/TicTacToeGameThread.java b/app/src/main/java/org/toop/app/game/TicTacToeGameThread.java new file mode 100644 index 0000000..a71bf07 --- /dev/null +++ b/app/src/main/java/org/toop/app/game/TicTacToeGameThread.java @@ -0,0 +1,177 @@ +package org.toop.app.game; + +import org.toop.app.App; +import org.toop.app.GameInformation; +import org.toop.app.canvas.TicTacToeCanvas; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.game.Game; +import org.toop.game.tictactoe.TicTacToe; +import org.toop.game.tictactoe.TicTacToeAI; + +import java.util.function.Consumer; + +import javafx.geometry.Pos; +import javafx.scene.paint.Color; + +public final class TicTacToeGameThread extends BaseGameThread { + public TicTacToeGameThread(GameInformation info, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage, Runnable onGameOver) { + super(info, myTurn, onForfeit, onExit, onMessage, onGameOver, + TicTacToe::new, + TicTacToeAI::new, + clickHandler -> new TicTacToeCanvas(Color.GRAY, (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3, clickHandler) + ); + } + + public TicTacToeGameThread(GameInformation info) { + this(info, 0, null, null, null, null); + } + + @Override + protected void addCanvasToPrimary() { + primary.add(Pos.CENTER, canvas.getCanvas()); + } + + @Override + protected int getCurrentTurn() { + return game.getCurrentTurn(); + } + + @Override + protected char getSymbolForTurn(int turn) { + return turn == 0 ? 'X' : 'O'; + } + + @Override + protected String getNameForTurn(int turn) { + return turn == 0 ? "X" : "O"; + } + + private void drawMove(Game.Move move) { + if (move.value() == 'X') canvas.drawX(Color.RED, move.position()); + else canvas.drawO(Color.BLUE, move.position()); + } + + @Override + protected void onMoveResponse(NetworkEvents.GameMoveResponse response) { + if (!isRunning.get()) { + return; + } + + char playerChar; + + if (response.player().equalsIgnoreCase(information.players[0].name)) { + playerChar = myTurn == 0? 'X' : 'O'; + } else { + playerChar = myTurn == 0? 'O' : 'X'; + } + + final Game.Move move = new Game.Move(Integer.parseInt(response.move()), playerChar); + final Game.State state = game.play(move); + + if (state != Game.State.NORMAL) { + if (state == Game.State.WIN) { + if (response.player().equalsIgnoreCase(information.players[0].name)) { + primary.gameOver(true, information.players[0].name); + gameOver(); + } else { + primary.gameOver(false, information.players[1].name); + gameOver(); + } + } else if (state == Game.State.DRAW) { + if (game.getLegalMoves().length == 0) { + primary.gameOver(false, ""); + gameOver(); + } + } + } + + drawMove(move); + setGameLabels(game.getCurrentTurn() == myTurn); + } + + @Override + protected void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { + if (!isRunning.get()) { + return; + } + + moveQueue.clear(); + + int position = -1; + + if (information.players[0].isHuman) { + try { + position = moveQueue.take().position(); + } catch (InterruptedException _) {} + } else { + final Game.Move move; + if (information.players[1].name.equalsIgnoreCase("pism")) { + move = ai.findWorstMove(game,9); + }else{ + move = ai.findBestMove(game, information.players[0].computerDifficulty); + } + + assert move != null; + position = move.position(); + } + + new EventFlow().addPostEvent(new NetworkEvents.SendMove(response.clientId(), (short)position)) + .postEvent(); + } + + @Override + protected void localGameThread() { + while (isRunning.get()) { + final int currentTurn = game.getCurrentTurn(); + setGameLabels(currentTurn == myTurn); + + Game.Move move = null; + + if (information.players[currentTurn].isHuman) { + try { + final Game.Move wants = moveQueue.take(); + final Game.Move[] legalMoves = game.getLegalMoves(); + + for (final Game.Move legalMove : legalMoves) { + if (legalMove.position() == wants.position() && + legalMove.value() == wants.value()) { + move = wants; + break; + } + } + } catch (InterruptedException _) {} + } else { + final long start = System.currentTimeMillis(); + + move = ai.findBestMove(game, information.players[currentTurn].computerDifficulty); + + if (information.players[currentTurn].computerThinkTime > 0) { + final long elapsedTime = System.currentTimeMillis() - start; + final long sleepTime = information.players[currentTurn].computerThinkTime * 1000L - elapsedTime; + + try { + Thread.sleep((long)(sleepTime * Math.random())); + } catch (InterruptedException _) {} + } + } + + if (move == null) { + continue; + } + + final Game.State state = game.play(move); + drawMove(move); + + if (state != Game.State.NORMAL) { + if (state == Game.State.WIN) { + primary.gameOver(information.players[currentTurn].isHuman, information.players[currentTurn].name); + } else if (state == Game.State.DRAW) { + primary.gameOver(false, ""); + } + + isRunning.set(false); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java b/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java index e99de78..b96751d 100644 --- a/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java +++ b/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java @@ -3,7 +3,7 @@ package org.toop.app.view.views; import org.toop.app.GameInformation; import org.toop.app.game.Connect4Game; import org.toop.app.game.ReversiGame; -import org.toop.app.game.TicTacToeGame; +import org.toop.app.game.TicTacToeGameThread; import org.toop.app.view.View; import org.toop.app.view.ViewStack; import org.toop.app.view.displays.SongDisplay; @@ -45,10 +45,10 @@ public final class LocalMultiplayerView extends View { } switch (information.type) { - case TICTACTOE: new TicTacToeGame(information); break; + case TICTACTOE: new TicTacToeGameThread(information); break; case REVERSI: new ReversiGame(information); break; case CONNECT4: new Connect4Game(information); break; - //case BATTLESHIP: new BattleshipGame(information); break; + // case BATTLESHIP: new BattleshipGame(information); break; } }); diff --git a/app/src/main/java/org/toop/app/widget/Primitive.java b/app/src/main/java/org/toop/app/widget/Primitive.java index 7fe8832..75795f2 100644 --- a/app/src/main/java/org/toop/app/widget/Primitive.java +++ b/app/src/main/java/org/toop/app/widget/Primitive.java @@ -23,8 +23,10 @@ public final class Primitive { var header = new Text(); header.getStyleClass().add("header"); - header.setText(AppContext.getString(key)); - header.textProperty().bind(AppContext.bindToKey(key)); + if (!key.isEmpty()) { + header.setText(AppContext.getString(key)); + header.textProperty().bind(AppContext.bindToKey(key)); + } return header; } @@ -33,8 +35,10 @@ public final class Primitive { var text = new Text(); text.getStyleClass().add("text"); - text.setText(AppContext.getString(key)); - text.textProperty().bind(AppContext.bindToKey(key)); + if (!key.isEmpty()) { + text.setText(AppContext.getString(key)); + text.textProperty().bind(AppContext.bindToKey(key)); + } return text; } @@ -43,8 +47,10 @@ public final class Primitive { var button = new Button(); button.getStyleClass().add("button"); - button.setText(AppContext.getString(key)); - button.textProperty().bind(AppContext.bindToKey(key)); + if (!key.isEmpty()) { + button.setText(AppContext.getString(key)); + button.textProperty().bind(AppContext.bindToKey(key)); + } if (onAction != null) { button.setOnAction(_ -> @@ -58,8 +64,10 @@ public final class Primitive { var input = new TextField(); input.getStyleClass().add("input"); - input.setPromptText(AppContext.getString(promptKey)); - input.promptTextProperty().bind(AppContext.bindToKey(promptKey)); + if (!promptKey.isEmpty()) { + input.setPromptText(AppContext.getString(promptKey)); + input.promptTextProperty().bind(AppContext.bindToKey(promptKey)); + } input.setText(text); @@ -133,7 +141,11 @@ public final class Primitive { hbox.getStyleClass().add("container"); hbox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); - hbox.getChildren().addAll(nodes); + for (var node : nodes) { + if (node != null) { + hbox.getChildren().add(node); + } + } return hbox; } @@ -143,7 +155,11 @@ public final class Primitive { vbox.getStyleClass().add("container"); vbox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); - vbox.getChildren().addAll(nodes); + for (var node : nodes) { + if (node != null) { + vbox.getChildren().add(node); + } + } return vbox; } diff --git a/app/src/main/java/org/toop/app/widget/WidgetContainer.java b/app/src/main/java/org/toop/app/widget/WidgetContainer.java index 1d56c43..aeeec92 100644 --- a/app/src/main/java/org/toop/app/widget/WidgetContainer.java +++ b/app/src/main/java/org/toop/app/widget/WidgetContainer.java @@ -1,19 +1,15 @@ package org.toop.app.widget; import org.toop.app.widget.complex.PopupWidget; -import org.toop.app.widget.complex.PrimaryWidget; - -import java.util.ArrayDeque; -import java.util.Deque; +import org.toop.app.widget.complex.ViewWidget; import javafx.application.Platform; import javafx.geometry.Pos; import javafx.scene.layout.StackPane; public final class WidgetContainer { - private static final Deque popups = new ArrayDeque<>(); - private static StackPane root; + private static ViewWidget currentView; public static synchronized StackPane setup() { if (root != null) { @@ -21,7 +17,7 @@ public final class WidgetContainer { } root = new StackPane(); - root.getStyleClass().add("bg-primary"); + root.getStyleClass().add("bg-view"); return root; } @@ -38,15 +34,14 @@ public final class WidgetContainer { StackPane.setAlignment(widget.getNode(), position); - if (widget instanceof PrimaryWidget) { - root.getChildren().addFirst(widget.getNode()); + if (widget instanceof ViewWidget view) { + root.getChildren().addFirst(view.getNode()); + currentView = view; + } else if (widget instanceof PopupWidget popup) { + currentView.add(Pos.CENTER, popup); } else { root.getChildren().add(widget.getNode()); } - - if (widget instanceof PopupWidget popup) { - popups.push(popup); - } }); } @@ -56,15 +51,15 @@ public final class WidgetContainer { } Platform.runLater(() -> { - root.getChildren().remove(widget.getNode()); - - if (widget instanceof PrimaryWidget) { - for (var popup : popups) { - root.getChildren().remove(popup.getNode()); - } - - popups.clear(); + if (widget instanceof PopupWidget popup) { + currentView.remove(popup); + } else { + root.getChildren().remove(widget.getNode()); } }); } + + public static ViewWidget getCurrentView() { + return currentView; + } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java b/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java index 107429c..e8719ce 100644 --- a/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java +++ b/app/src/main/java/org/toop/app/widget/complex/ConfirmWidget.java @@ -7,14 +7,21 @@ import javafx.application.Platform; import javafx.scene.Node; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; +import javafx.scene.text.Text; public class ConfirmWidget implements Widget { private final HBox buttonsContainer; + private final Text messageText; private final VBox container; public ConfirmWidget(String confirm) { buttonsContainer = Primitive.hbox(); - container = Primitive.vbox(Primitive.header(confirm), buttonsContainer); + messageText = Primitive.text(""); + container = Primitive.vbox(Primitive.header(confirm), messageText, Primitive.separator(), buttonsContainer); + } + + public void setMessage(String message) { + messageText.setText(message); } public void addButton(String key, Runnable onClick) { diff --git a/app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java b/app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java new file mode 100644 index 0000000..90ec2a5 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/PlayerInfoWidget.java @@ -0,0 +1,83 @@ +package org.toop.app.widget.complex; + +import org.toop.app.GameInformation; +import org.toop.app.widget.Primitive; + +import javafx.scene.Node; +import javafx.scene.layout.VBox; + +public class PlayerInfoWidget { + private final GameInformation.Player information; + private final VBox container; + + public PlayerInfoWidget(GameInformation.Player information) { + this.information = information; + container = Primitive.vbox( + buildToggle().getNode(), + buildContent() + ); + } + + private ToggleWidget buildToggle() { + return new ToggleWidget( + "computer", "player", + information.isHuman, + isHuman -> { + information.isHuman = isHuman; + container.getChildren().setAll( + buildToggle().getNode(), + buildContent() + ); + } + ); + } + + private Node buildContent() { + if (information.isHuman) { + var nameInput = new LabeledInputWidget( + "name", + "enter-your-name", + information.name, + newName -> information.name = newName + ); + + return nameInput.getNode(); + } else { + if (information.name == null || information.name.isEmpty()) { + information.name = "Pism Bot"; + } + + var playerName = Primitive.text(""); + playerName.setText(information.name); + + var nameDisplay = Primitive.vbox( + Primitive.text("name"), + playerName + ); + + var difficultySlider = new LabeledSliderWidget( + "computer-difficulty", + 0, 5, + information.computerDifficulty, + newVal -> information.computerDifficulty = newVal + ); + + var thinkTimeSlider = new LabeledSliderWidget( + "computer-think-time", + 0, 5, + information.computerThinkTime, + newVal -> information.computerThinkTime = newVal + ); + + return Primitive.vbox( + nameDisplay, + difficultySlider.getNode(), + thinkTimeSlider.getNode() + ); + } + } + + public Node getNode() { + return container; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java b/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java deleted file mode 100644 index 848e76e..0000000 --- a/app/src/main/java/org/toop/app/widget/complex/PrimaryWidget.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.toop.app.widget.complex; - -import javafx.geometry.Pos; -import org.toop.app.widget.Primitive; - -public abstract class PrimaryWidget extends StackWidget { - private PrimaryWidget previous = null; - - public PrimaryWidget() { - super("bg-primary"); - } - - public void transitionNext(PrimaryWidget primary) { - primary.previous = this; - replace(Pos.CENTER, primary); - - var backButton = Primitive.button("back", () -> { - primary.transitionPrevious(); - }); - - primary.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton)); - } - - public void transitionPrevious() { - if (previous == null) { - return; - } - - replace(Pos.CENTER, previous); - previous = null; - } - - public void reload(PrimaryWidget primary) { - primary.previous = previous; - replace(Pos.CENTER, primary); - - var backButton = Primitive.button("back", () -> { - primary.transitionPrevious(); - }); - - primary.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton)); - } -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java b/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java new file mode 100644 index 0000000..ad9265c --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/complex/ViewWidget.java @@ -0,0 +1,49 @@ +package org.toop.app.widget.complex; + +import org.toop.app.widget.Primitive; + +import javafx.geometry.Pos; + +public abstract class ViewWidget extends StackWidget { + private ViewWidget previous = null; + + public ViewWidget() { + super("bg-primary"); + } + + public void transition(ViewWidget view) { + view.previous = this; + replace(Pos.CENTER, view); + } + + public void transitionNext(ViewWidget view) { + view.previous = this; + replace(Pos.CENTER, view); + + var backButton = Primitive.button("back", () -> { + view.transitionPrevious(); + }); + + view.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton)); + } + + public void transitionPrevious() { + if (previous == null) { + return; + } + + replace(Pos.CENTER, previous); + previous = null; + } + + public void reload(ViewWidget view) { + view.previous = previous; + replace(Pos.CENTER, view); + + var backButton = Primitive.button("back", () -> { + view.transitionPrevious(); + }); + + view.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton)); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/popup/ChallengePopup.java b/app/src/main/java/org/toop/app/widget/popup/ChallengePopup.java new file mode 100644 index 0000000..1a2f755 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/popup/ChallengePopup.java @@ -0,0 +1,60 @@ +package org.toop.app.widget.popup; + +import org.toop.app.GameInformation; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.PlayerInfoWidget; +import org.toop.app.widget.complex.PopupWidget; + +import java.util.function.Consumer; + +import javafx.geometry.Pos; + +public final class ChallengePopup extends PopupWidget { + private final GameInformation.Player playerInformation; + private final String challenger; + private final String game; + private final Consumer onAccept; + + public ChallengePopup(String challenger, String game, Consumer onAccept) { + this.challenger = challenger; + this.game = game; + this.onAccept = onAccept; + + this.playerInformation = new GameInformation.Player(); + + setupLayout(); + } + + private void setupLayout() { + var challengeText = Primitive.text("you-were-challenged-by"); + + var challengerHeader = Primitive.header(""); + challengerHeader.setText(challenger); + + var gameText = Primitive.text("to-a-game-of"); + gameText.setText(gameText.getText() + " " + game); + + var acceptButton = Primitive.button("accept", () -> onAccept.accept(playerInformation)); + var denyButton = Primitive.button("deny", () -> hide()); + + var leftSection = Primitive.vbox( + challengeText, + challengerHeader, + gameText, + Primitive.separator(), + Primitive.hbox( + acceptButton, + denyButton + ) + ); + + var playerInfoWidget = new PlayerInfoWidget(playerInformation); + + add(Pos.CENTER, + Primitive.hbox( + leftSection, + playerInfoWidget.getNode() + ) + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/popup/ErrorPopup.java b/app/src/main/java/org/toop/app/widget/popup/ErrorPopup.java new file mode 100644 index 0000000..89d10c6 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/popup/ErrorPopup.java @@ -0,0 +1,16 @@ +package org.toop.app.widget.popup; + +import org.toop.app.widget.complex.ConfirmWidget; +import org.toop.app.widget.complex.PopupWidget; + +import javafx.geometry.Pos; + +public class ErrorPopup extends PopupWidget { + public ErrorPopup(String error) { + var confirmWidget = new ConfirmWidget("error"); + confirmWidget.setMessage(error); + confirmWidget.addButton("ok", this::hide); + + add(Pos.CENTER, confirmWidget); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/popup/GameOverPopup.java b/app/src/main/java/org/toop/app/widget/popup/GameOverPopup.java new file mode 100644 index 0000000..f642272 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/popup/GameOverPopup.java @@ -0,0 +1,25 @@ +package org.toop.app.widget.popup; + +import org.toop.app.widget.complex.ConfirmWidget; +import org.toop.app.widget.complex.PopupWidget; +import org.toop.local.AppContext; + +import javafx.geometry.Pos; + +public final class GameOverPopup extends PopupWidget { + public GameOverPopup(boolean iWon, String winner) { + var confirmWidget = new ConfirmWidget("game-over"); + + if (winner.isEmpty()) { + confirmWidget.setMessage(AppContext.getString("the-game-ended-in-a-draw")); + } else if (iWon) { + confirmWidget.setMessage(AppContext.getString("you-win")); + } else { + confirmWidget.setMessage(AppContext.getString("you-lost-against") + ": " + winner); + } + + confirmWidget.addButton("ok", () -> hide()); + + add(Pos.CENTER, confirmWidget); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/popup/SendChallengePopup.java b/app/src/main/java/org/toop/app/widget/popup/SendChallengePopup.java new file mode 100644 index 0000000..f8754fd --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/popup/SendChallengePopup.java @@ -0,0 +1,90 @@ +package org.toop.app.widget.popup; + +import org.toop.app.GameInformation; +import org.toop.app.Server; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.LabeledChoiceWidget; +import org.toop.app.widget.complex.PlayerInfoWidget; +import org.toop.app.widget.complex.PopupWidget; +import org.toop.local.AppContext; + +import java.util.function.BiConsumer; + +import javafx.geometry.Pos; +import javafx.util.StringConverter; + +public final class SendChallengePopup extends PopupWidget { + private final Server server; + private final String opponent; + private final BiConsumer onSend; + + private final GameInformation.Player playerInformation; + + public SendChallengePopup(Server server, String opponent, BiConsumer onSend) { + this.server = server; + this.opponent = opponent; + this.onSend = onSend; + + this.playerInformation = new GameInformation.Player(); + + setupLayout(); + } + + private void setupLayout() { + // --- Left side: challenge text and buttons --- + var challengeText = Primitive.text("challenge"); + + var opponentHeader = Primitive.header(opponent); + + var gameText = Primitive.text("to-a-game-of"); + + var games = server.getGameList(); + var gameChoice = new LabeledChoiceWidget<>( + "game", + new StringConverter<>() { + @Override + public String toString(String game) { + return AppContext.getString(game); + } + @Override + public String fromString(String s) { return null; } + }, + games.getFirst(), + newGame -> { + playerInformation.computerDifficulty = Math.min( + playerInformation.computerDifficulty, + GameInformation.Type.maxDepth(Server.gameToType(newGame)) + ); + }, + games.toArray(new String[0]) + ); + + var sendButton = Primitive.button( + "send", + () -> onSend.accept(playerInformation, gameChoice.getValue()) + ); + + var cancelButton = Primitive.button("cancel", () -> hide()); + + var leftSection = Primitive.vbox( + challengeText, + opponentHeader, + gameText, + gameChoice.getNode(), + Primitive.separator(), + Primitive.hbox( + sendButton, + cancelButton + ) + ); + + var playerInfoWidget = new PlayerInfoWidget(playerInformation); + + add(Pos.CENTER, + Primitive.hbox( + leftSection, + playerInfoWidget.getNode() + ) + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/primary/LocalPrimary.java b/app/src/main/java/org/toop/app/widget/primary/LocalPrimary.java deleted file mode 100644 index ef0364f..0000000 --- a/app/src/main/java/org/toop/app/widget/primary/LocalPrimary.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.toop.app.widget.primary; - -import org.toop.app.widget.Primitive; -import org.toop.app.widget.complex.PrimaryWidget; - -import javafx.geometry.Pos; - -public class LocalPrimary extends PrimaryWidget { - public LocalPrimary() { - var ticTacToeButton = Primitive.button("tic-tac-toe", () -> { - }); - - var reversiButton = Primitive.button("reversi", () -> { - }); - - var connect4Button = Primitive.button("connect4", () -> { - }); - - add(Pos.CENTER, Primitive.vbox( - ticTacToeButton, - reversiButton, - connect4Button - )); - } -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/primary/CreditsPrimary.java b/app/src/main/java/org/toop/app/widget/view/CreditsView.java similarity index 92% rename from app/src/main/java/org/toop/app/widget/primary/CreditsPrimary.java rename to app/src/main/java/org/toop/app/widget/view/CreditsView.java index 82590cb..f7aaf01 100644 --- a/app/src/main/java/org/toop/app/widget/primary/CreditsPrimary.java +++ b/app/src/main/java/org/toop/app/widget/view/CreditsView.java @@ -1,8 +1,8 @@ -package org.toop.app.widget.primary; +package org.toop.app.widget.view; import org.toop.app.App; import org.toop.app.widget.Primitive; -import org.toop.app.widget.complex.PrimaryWidget; +import org.toop.app.widget.complex.ViewWidget; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; @@ -14,8 +14,8 @@ import javafx.scene.layout.Region; import javafx.scene.text.Text; import javafx.util.Duration; -public class CreditsPrimary extends PrimaryWidget { - public CreditsPrimary() { +public class CreditsView extends ViewWidget { + public CreditsView() { var scrumMasterCredit = newCredit("scrum-master", "Stef"); var productOwnerCredit = newCredit("product-owner", "Omar"); var mergeCommanderCredit = newCredit("merge-commander", "Bas"); diff --git a/app/src/main/java/org/toop/app/widget/view/GameView.java b/app/src/main/java/org/toop/app/widget/view/GameView.java new file mode 100644 index 0000000..82778b9 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/view/GameView.java @@ -0,0 +1,96 @@ +package org.toop.app.widget.view; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.ViewWidget; +import org.toop.app.widget.popup.GameOverPopup; + +import java.util.function.Consumer; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.text.Text; + +public final class GameView extends ViewWidget { + private final Text currentPlayerHeader; + private final Text currentMoveHeader; + private final Text nextPlayerHeader; + + private final Button forfeitButton; + private final Button exitButton; + + private final TextField chatInput; + + public GameView(Runnable onForfeit, Runnable onExit, Consumer onMessage) { + currentPlayerHeader = Primitive.header(""); + currentMoveHeader = Primitive.header(""); + nextPlayerHeader = Primitive.header(""); + + if (onForfeit != null) { + forfeitButton = Primitive.button("forfeit", () -> onForfeit.run()); + } else { + forfeitButton = null; + } + + exitButton = Primitive.button("exit", () -> { + onExit.run(); + transitionPrevious(); + }); + + if (onMessage != null) { + chatInput = Primitive.input("enter-your-message", "", null); + chatInput.setOnAction(_ -> { + onMessage.accept(chatInput.getText()); + chatInput.clear(); + }); + } else { + chatInput = null; + } + + setupLayout(); + } + + private void setupLayout() { + var playerInfo = Primitive.vbox( + currentPlayerHeader, + Primitive.hbox( + Primitive.separator(), + currentMoveHeader, + Primitive.separator() + ), + nextPlayerHeader + ); + + add(Pos.TOP_RIGHT, playerInfo); + + var buttons = Primitive.vbox( + forfeitButton, + exitButton + ); + + add(Pos.BOTTOM_LEFT, buttons); + + if (chatInput != null) { + add(Pos.BOTTOM_RIGHT, Primitive.vbox(chatInput)); + } + } + + public void nextPlayer(boolean isMe, String currentPlayer, String currentMove, String nextPlayer) { + Platform.runLater(() -> { + currentPlayerHeader.setText(currentPlayer); + currentMoveHeader.setText(currentMove); + nextPlayerHeader.setText(nextPlayer); + + if (isMe) { + currentPlayerHeader.getStyleClass().add("my-turn"); + } else { + currentPlayerHeader.getStyleClass().remove("my-turn"); + } + }); + } + + public void gameOver(boolean iWon, String winner) { + new GameOverPopup(iWon, winner).show(Pos.CENTER); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java b/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java new file mode 100644 index 0000000..609a260 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/view/LocalMultiplayerView.java @@ -0,0 +1,74 @@ +package org.toop.app.widget.view; + +import org.toop.app.GameInformation; +import org.toop.app.game.Connect4Game; +import org.toop.app.game.ReversiGame; +import org.toop.app.game.TicTacToeGameThread; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.PlayerInfoWidget; +import org.toop.app.widget.complex.ViewWidget; +import org.toop.app.widget.popup.ErrorPopup; +import org.toop.local.AppContext; + +import javafx.geometry.Pos; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.VBox; + +public class LocalMultiplayerView extends ViewWidget { + private final GameInformation information; + + public LocalMultiplayerView(GameInformation.Type type) { + this(new GameInformation(type)); + } + + public LocalMultiplayerView(GameInformation information) { + this.information = information; + var playButton = Primitive.button("play", () -> { + for (var player : information.players) { + if (player.isHuman && player.name.isEmpty()) { + new ErrorPopup(AppContext.getString("please-enter-your-name")).show(Pos.CENTER); + return; + } + } + + switch (information.type) { + case TICTACTOE -> new TicTacToeGameThread(information); + case REVERSI -> new ReversiGame(information); + case CONNECT4 -> new Connect4Game(information); + // case BATTLESHIP -> new BattleshipGame(information); + } + }); + + var playerSection = setupPlayerSections(); + + add(Pos.CENTER, Primitive.vbox( + playerSection, + Primitive.separator(), + playButton + )); + } + + private ScrollPane setupPlayerSections() { + int playerCount = GameInformation.Type.playerCount(information.type); + VBox[] playerBoxes = new VBox[playerCount]; + + for (int i = 0; i < playerCount; i++) { + var player = information.players[i]; + + var playerHeader = Primitive.header(""); + playerHeader.setText("player" + " #" + (i + 1)); + + var playerWidget = new PlayerInfoWidget(player); + + playerBoxes[i] = Primitive.vbox( + playerHeader, + Primitive.separator(), + playerWidget.getNode() + ); + } + + return Primitive.scroll(Primitive.hbox( + playerBoxes + )); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/view/LocalView.java b/app/src/main/java/org/toop/app/widget/view/LocalView.java new file mode 100644 index 0000000..90cfac6 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/view/LocalView.java @@ -0,0 +1,29 @@ +package org.toop.app.widget.view; + +import org.toop.app.GameInformation; +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.ViewWidget; + +import javafx.geometry.Pos; + +public class LocalView extends ViewWidget { + public LocalView() { + var ticTacToeButton = Primitive.button("tic-tac-toe", () -> { + transitionNext(new LocalMultiplayerView(GameInformation.Type.TICTACTOE)); + }); + + var reversiButton = Primitive.button("reversi", () -> { + transitionNext(new LocalMultiplayerView(GameInformation.Type.REVERSI)); + }); + + var connect4Button = Primitive.button("connect4", () -> { + transitionNext(new LocalMultiplayerView(GameInformation.Type.CONNECT4)); + }); + + add(Pos.CENTER, Primitive.vbox( + ticTacToeButton, + reversiButton, + connect4Button + )); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/widget/primary/MainPrimary.java b/app/src/main/java/org/toop/app/widget/view/MainView.java similarity index 63% rename from app/src/main/java/org/toop/app/widget/primary/MainPrimary.java rename to app/src/main/java/org/toop/app/widget/view/MainView.java index 06ecdf0..ff7a710 100644 --- a/app/src/main/java/org/toop/app/widget/primary/MainPrimary.java +++ b/app/src/main/java/org/toop/app/widget/view/MainView.java @@ -1,27 +1,27 @@ -package org.toop.app.widget.primary; +package org.toop.app.widget.view; import org.toop.app.App; import org.toop.app.widget.Primitive; -import org.toop.app.widget.complex.PrimaryWidget; +import org.toop.app.widget.complex.ViewWidget; import javafx.geometry.Pos; -public class MainPrimary extends PrimaryWidget { - public MainPrimary() { +public class MainView extends ViewWidget { + public MainView() { var localButton = Primitive.button("local", () -> { - transitionNext(new LocalPrimary()); + transitionNext(new LocalView()); }); var onlineButton = Primitive.button("online", () -> { - transitionNext(new OnlinePrimary()); + transitionNext(new OnlineView()); }); var creditsButton = Primitive.button("credits", () -> { - transitionNext(new CreditsPrimary()); + transitionNext(new CreditsView()); }); var optionsButton = Primitive.button("options", () -> { - transitionNext(new OptionsPrimary()); + transitionNext(new OptionsView()); }); var quitButton = Primitive.button("quit", () -> { diff --git a/app/src/main/java/org/toop/app/widget/primary/OnlinePrimary.java b/app/src/main/java/org/toop/app/widget/view/OnlineView.java similarity index 84% rename from app/src/main/java/org/toop/app/widget/primary/OnlinePrimary.java rename to app/src/main/java/org/toop/app/widget/view/OnlineView.java index 98bc68e..16ed1df 100644 --- a/app/src/main/java/org/toop/app/widget/primary/OnlinePrimary.java +++ b/app/src/main/java/org/toop/app/widget/view/OnlineView.java @@ -1,14 +1,14 @@ -package org.toop.app.widget.primary; +package org.toop.app.widget.view; import org.toop.app.Server; import org.toop.app.widget.Primitive; import org.toop.app.widget.complex.LabeledInputWidget; -import org.toop.app.widget.complex.PrimaryWidget; +import org.toop.app.widget.complex.ViewWidget; import javafx.geometry.Pos; -public class OnlinePrimary extends PrimaryWidget { - public OnlinePrimary() { +public class OnlineView extends ViewWidget { + public OnlineView() { var serverInformationHeader = Primitive.header("server-information"); var serverIPInput = new LabeledInputWidget("ip-address", "enter-the-server-ip", "", _ -> {}); diff --git a/app/src/main/java/org/toop/app/widget/primary/OptionsPrimary.java b/app/src/main/java/org/toop/app/widget/view/OptionsView.java similarity index 95% rename from app/src/main/java/org/toop/app/widget/primary/OptionsPrimary.java rename to app/src/main/java/org/toop/app/widget/view/OptionsView.java index 9061df4..198f75b 100644 --- a/app/src/main/java/org/toop/app/widget/primary/OptionsPrimary.java +++ b/app/src/main/java/org/toop/app/widget/view/OptionsView.java @@ -1,10 +1,10 @@ -package org.toop.app.widget.primary; +package org.toop.app.widget.view; import org.toop.app.App; import org.toop.app.widget.Primitive; import org.toop.app.widget.complex.LabeledChoiceWidget; import org.toop.app.widget.complex.LabeledSliderWidget; -import org.toop.app.widget.complex.PrimaryWidget; +import org.toop.app.widget.complex.ViewWidget; import org.toop.app.widget.complex.ToggleWidget; import org.toop.framework.audio.VolumeControl; import org.toop.framework.audio.events.AudioEvents; @@ -18,8 +18,8 @@ import javafx.geometry.Pos; import javafx.scene.layout.VBox; import javafx.util.StringConverter; -public class OptionsPrimary extends PrimaryWidget { - public OptionsPrimary() { +public class OptionsView extends ViewWidget { + public OptionsView() { add(Pos.CENTER, Primitive.hbox( generalSection(), volumeSection(), @@ -42,7 +42,7 @@ public class OptionsPrimary extends PrimaryWidget { newLocale -> { AppSettings.getSettings().setLocale(newLocale.toString()); AppContext.setLocale(newLocale); - reload(new OptionsPrimary()); + reload(new OptionsView()); }, AppContext.getLocalization().getAvailableLocales().toArray(new Locale[0]) ); diff --git a/app/src/main/java/org/toop/app/widget/view/ServerView.java b/app/src/main/java/org/toop/app/widget/view/ServerView.java new file mode 100644 index 0000000..d69c036 --- /dev/null +++ b/app/src/main/java/org/toop/app/widget/view/ServerView.java @@ -0,0 +1,60 @@ +package org.toop.app.widget.view; + +import org.toop.app.widget.Primitive; +import org.toop.app.widget.complex.ViewWidget; + +import java.util.List; +import java.util.function.Consumer; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ListView; + +public final class ServerView extends ViewWidget { + private final String user; + private final Consumer onPlayerClicked; + private final Runnable onDisconnect; + + private final ListView