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