From 60283b69816b6792ab517089707cfe4d4a099e9a Mon Sep 17 00:00:00 2001 From: Juuxel <6596629+Juuxel@users.noreply.github.com> Date: Sat, 11 Sep 2021 01:24:34 +0300 Subject: Add observable properties and narration support (#136) * Add focused narration support * yes! * More work on hovering and observables * Fix WWidget.setHovered javadoc * Document ObservableProperty.add/removeListener * Add observable views * Fix ObservableView.ChangeListener javadoc * More JD! * Add taglet for listing observable properties Overkill? Yep. Still fancy? Absolutely! * Fix narration element indices and more JD tricks * Add slot hovering narration * WItemSlot.getExtraNarrationMessage -> getNarrationName, make public API * Remove the binding functionality of observable properties This is required for having proper change listeners. * Add ObservableView.hasValue * Add some utility methods to ObservableView * Clarify ObservableView.ChangeListener parameters * Remove properties tag from WButton javadoc --- .../cotton/gui/client/CottonClientScreen.java | 22 ++- .../cotton/gui/client/CottonInventoryScreen.java | 26 +++- .../cotton/gui/impl/client/CottonScreenImpl.java | 3 + .../cotton/gui/impl/client/MouseInputHandler.java | 38 +++-- .../cotton/gui/impl/client/NarrationHelper.java | 49 +++++++ .../cotton/gui/impl/client/NarrationMessages.java | 29 ++++ .../cotton/gui/widget/WAbstractSlider.java | 11 ++ .../github/cottonmc/cotton/gui/widget/WButton.java | 17 +++ .../cottonmc/cotton/gui/widget/WItemSlot.java | 59 +++++++- .../github/cottonmc/cotton/gui/widget/WLabel.java | 8 + .../cottonmc/cotton/gui/widget/WLabeledSlider.java | 15 ++ .../github/cottonmc/cotton/gui/widget/WPanel.java | 12 ++ .../cotton/gui/widget/WPlayerInvPanel.java | 9 +- .../cottonmc/cotton/gui/widget/WScrollBar.java | 10 ++ .../cottonmc/cotton/gui/widget/WTabPanel.java | 17 +++ .../github/cottonmc/cotton/gui/widget/WText.java | 8 + .../cottonmc/cotton/gui/widget/WTextField.java | 15 +- .../cottonmc/cotton/gui/widget/WToggleButton.java | 25 ++++ .../github/cottonmc/cotton/gui/widget/WWidget.java | 69 +++++++++ .../cotton/gui/widget/data/ObservableProperty.java | 162 +++++++++++++++++++++ .../cotton/gui/widget/data/ObservableView.java | 75 ++++++++++ 21 files changed, 652 insertions(+), 27 deletions(-) create mode 100644 src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationHelper.java create mode 100644 src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationMessages.java create mode 100644 src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableProperty.java create mode 100644 src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableView.java (limited to 'src/main/java/io') diff --git a/src/main/java/io/github/cottonmc/cotton/gui/client/CottonClientScreen.java b/src/main/java/io/github/cottonmc/cotton/gui/client/CottonClientScreen.java index cb80707..331f68b 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/client/CottonClientScreen.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/client/CottonClientScreen.java @@ -1,6 +1,7 @@ package io.github.cottonmc.cotton.gui.client; import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.LiteralText; import net.minecraft.text.Style; @@ -10,6 +11,7 @@ import io.github.cottonmc.cotton.gui.GuiDescription; import io.github.cottonmc.cotton.gui.impl.VisualLogger; import io.github.cottonmc.cotton.gui.impl.client.CottonScreenImpl; import io.github.cottonmc.cotton.gui.impl.client.MouseInputHandler; +import io.github.cottonmc.cotton.gui.impl.client.NarrationHelper; import io.github.cottonmc.cotton.gui.widget.WPanel; import io.github.cottonmc.cotton.gui.widget.WWidget; import org.jetbrains.annotations.Nullable; @@ -36,6 +38,8 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl { @Nullable protected WWidget lastResponder = null; + + private final MouseInputHandler mouseInputHandler = new MouseInputHandler<>(this); public CottonClientScreen(GuiDescription description) { this(new LiteralText(""), description); @@ -46,7 +50,8 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl { this.description = description; description.getRootPanel().validate(description); } - + + @Override public GuiDescription getDescription() { return description; } @@ -176,7 +181,7 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl { int containerX = (int)mouseX-left; int containerY = (int)mouseY-top; if (containerX<0 || containerY<0 || containerX>=width || containerY>=height) return true; - MouseInputHandler.onMouseDown(description, this, containerX, containerY, mouseButton); + mouseInputHandler.onMouseDown(containerX, containerY, mouseButton); return true; } @@ -187,7 +192,7 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl { super.mouseReleased(mouseX, mouseY, mouseButton); int containerX = (int)mouseX-left; int containerY = (int)mouseY-top; - MouseInputHandler.onMouseUp(description, this, containerX, containerY, mouseButton); + mouseInputHandler.onMouseUp(containerX, containerY, mouseButton); return true; } @@ -199,7 +204,7 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl { int containerX = (int)mouseX-left; int containerY = (int)mouseY-top; - MouseInputHandler.onMouseDrag(description, this, containerX, containerY, mouseButton, deltaX, deltaY); + mouseInputHandler.onMouseDrag(containerX, containerY, mouseButton, deltaX, deltaY); return true; } @@ -210,7 +215,7 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl { int containerX = (int)mouseX-left; int containerY = (int)mouseY-top; - MouseInputHandler.onMouseScroll(description, containerX, containerY, amount); + mouseInputHandler.onMouseScroll(containerX, containerY, amount); return true; } @@ -221,7 +226,7 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl { int containerX = (int)mouseX-left; int containerY = (int)mouseY-top; - MouseInputHandler.onMouseMove(description, containerX, containerY); + mouseInputHandler.onMouseMove(containerX, containerY); } @Override @@ -264,4 +269,9 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl { return true; } + + @Override + protected void addElementNarrations(NarrationMessageBuilder builder) { + if (description != null) NarrationHelper.addNarrations(description.getRootPanel(), builder); + } } diff --git a/src/main/java/io/github/cottonmc/cotton/gui/client/CottonInventoryScreen.java b/src/main/java/io/github/cottonmc/cotton/gui/client/CottonInventoryScreen.java index 87d9453..953563c 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/client/CottonInventoryScreen.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/client/CottonInventoryScreen.java @@ -1,6 +1,7 @@ package io.github.cottonmc.cotton.gui.client; import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; import net.minecraft.client.render.DiffuseLighting; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.entity.player.PlayerEntity; @@ -8,12 +9,15 @@ import net.minecraft.text.LiteralText; import net.minecraft.text.Style; import net.minecraft.text.Text; +import io.github.cottonmc.cotton.gui.GuiDescription; import io.github.cottonmc.cotton.gui.SyncedGuiDescription; import io.github.cottonmc.cotton.gui.impl.VisualLogger; import io.github.cottonmc.cotton.gui.impl.client.CottonScreenImpl; import io.github.cottonmc.cotton.gui.impl.client.MouseInputHandler; +import io.github.cottonmc.cotton.gui.impl.client.NarrationHelper; import io.github.cottonmc.cotton.gui.widget.WPanel; import io.github.cottonmc.cotton.gui.widget.WWidget; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; import org.lwjgl.opengl.GL11; @@ -26,6 +30,7 @@ import org.lwjgl.opengl.GL11; public class CottonInventoryScreen extends HandledScreen implements CottonScreenImpl { protected SyncedGuiDescription description; @Nullable protected WWidget lastResponder = null; + private final MouseInputHandler> mouseInputHandler = new MouseInputHandler<>(this); /** * Constructs a new screen without a title. @@ -83,6 +88,12 @@ public class CottonInventoryScreen extends Handl VisualLogger.reset(); } + @ApiStatus.Internal + @Override + public GuiDescription getDescription() { + return description; + } + @Nullable @Override public WWidget getLastResponder() { @@ -187,7 +198,7 @@ public class CottonInventoryScreen extends Handl int containerX = (int)mouseX-x; int containerY = (int)mouseY-y; if (containerX<0 || containerY<0 || containerX>=width || containerY>=height) return result; - MouseInputHandler.onMouseDown(description, this, containerX, containerY, mouseButton); + mouseInputHandler.onMouseDown(containerX, containerY, mouseButton); return true; } @@ -197,7 +208,7 @@ public class CottonInventoryScreen extends Handl super.mouseReleased(mouseX, mouseY, mouseButton); int containerX = (int)mouseX-x; int containerY = (int)mouseY-y; - MouseInputHandler.onMouseUp(description, this, containerX, containerY, mouseButton); + mouseInputHandler.onMouseUp(containerX, containerY, mouseButton); return true; } @@ -208,7 +219,7 @@ public class CottonInventoryScreen extends Handl int containerX = (int)mouseX-x; int containerY = (int)mouseY-y; - MouseInputHandler.onMouseDrag(description, this, containerX, containerY, mouseButton, deltaX, deltaY); + mouseInputHandler.onMouseDrag(containerX, containerY, mouseButton, deltaX, deltaY); return true; } @@ -219,7 +230,7 @@ public class CottonInventoryScreen extends Handl int containerX = (int)mouseX-x; int containerY = (int)mouseY-y; - MouseInputHandler.onMouseScroll(description, containerX, containerY, amount); + mouseInputHandler.onMouseScroll(containerX, containerY, amount); return true; } @@ -230,7 +241,7 @@ public class CottonInventoryScreen extends Handl int containerX = (int)mouseX-x; int containerY = (int)mouseY-y; - MouseInputHandler.onMouseMove(description, containerX, containerY); + mouseInputHandler.onMouseMove(containerX, containerY); } @Override @@ -304,4 +315,9 @@ public class CottonInventoryScreen extends Handl return true; } + + @Override + protected void addElementNarrations(NarrationMessageBuilder builder) { + if (description != null) NarrationHelper.addNarrations(description.getRootPanel(), builder); + } } diff --git a/src/main/java/io/github/cottonmc/cotton/gui/impl/client/CottonScreenImpl.java b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/CottonScreenImpl.java index 2ef9632..cd215b3 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/impl/client/CottonScreenImpl.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/CottonScreenImpl.java @@ -5,11 +5,14 @@ import net.fabricmc.api.Environment; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.Style; +import io.github.cottonmc.cotton.gui.GuiDescription; import io.github.cottonmc.cotton.gui.widget.WWidget; import org.jetbrains.annotations.Nullable; @Environment(EnvType.CLIENT) public interface CottonScreenImpl { + GuiDescription getDescription(); + @Nullable WWidget getLastResponder(); diff --git a/src/main/java/io/github/cottonmc/cotton/gui/impl/client/MouseInputHandler.java b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/MouseInputHandler.java index 45fc17f..055df75 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/impl/client/MouseInputHandler.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/MouseInputHandler.java @@ -2,9 +2,9 @@ package io.github.cottonmc.cotton.gui.impl.client; import net.minecraft.client.gui.screen.Screen; -import io.github.cottonmc.cotton.gui.GuiDescription; import io.github.cottonmc.cotton.gui.widget.WWidget; import io.github.cottonmc.cotton.gui.widget.data.InputResult; +import io.github.cottonmc.cotton.gui.widget.data.ObservableProperty; import org.jetbrains.annotations.Nullable; import java.util.function.Function; @@ -12,10 +12,21 @@ import java.util.function.Function; /** * The implementation for mouse inputs. */ -public final class MouseInputHandler { - public static void onMouseDown(GuiDescription description, CottonScreenImpl screen, int containerX, int containerY, int mouseButton) { +public final class MouseInputHandler { + private final S screen; + private final ObservableProperty hovered = ObservableProperty.of(null); + + public MouseInputHandler(S screen) { + this.screen = screen; + hovered.addListener((property, from, to) -> { + if (from != null) from.setHovered(false); + if (to != null) to.setHovered(true); + }); + } + + public void onMouseDown(int containerX, int containerY, int mouseButton) { if (screen.getLastResponder() == null) { - WWidget lastResponder = description.getRootPanel().hit(containerX, containerY); + WWidget lastResponder = screen.getDescription().getRootPanel().hit(containerX, containerY); screen.setLastResponder(lastResponder); if (lastResponder != null) { runTree( @@ -28,7 +39,7 @@ public final class MouseInputHandler { } } - public static void onMouseUp(GuiDescription description, S screen, int containerX, int containerY, int mouseButton) { + public void onMouseUp(int containerX, int containerY, int mouseButton) { WWidget lastResponder = screen.getLastResponder(); if (lastResponder != null) { @@ -48,7 +59,7 @@ public final class MouseInputHandler { } } else { runTree( - description.getRootPanel().hit(containerX, containerY), + screen.getDescription().getRootPanel().hit(containerX, containerY), widget -> widget.onMouseUp(containerX - widget.getAbsoluteX(), containerY - widget.getAbsoluteY(), mouseButton) ); } @@ -56,7 +67,7 @@ public final class MouseInputHandler { screen.setLastResponder(null); } - public static void onMouseDrag(GuiDescription description, S screen, int containerX, int containerY, int mouseButton, double deltaX, double deltaY) { + public void onMouseDrag(int containerX, int containerY, int mouseButton, double deltaX, double deltaY) { WWidget lastResponder = screen.getLastResponder(); if (lastResponder != null) { @@ -68,22 +79,25 @@ public final class MouseInputHandler { if (containerX < 0 || containerY < 0 || containerX >= width || containerY >= height) return; runTree( - description.getRootPanel().hit(containerX, containerY), + screen.getDescription().getRootPanel().hit(containerX, containerY), widget -> widget.onMouseDrag(containerX - widget.getAbsoluteX(), containerY - widget.getAbsoluteY(), mouseButton, deltaX, deltaY) ); } } - public static void onMouseScroll(GuiDescription description, int containerX, int containerY, double amount) { + public void onMouseScroll(int containerX, int containerY, double amount) { runTree( - description.getRootPanel().hit(containerX, containerY), + screen.getDescription().getRootPanel().hit(containerX, containerY), widget -> widget.onMouseScroll(containerX - widget.getAbsoluteX(), containerY - widget.getAbsoluteY(), amount) ); } - public static void onMouseMove(GuiDescription description, int containerX, int containerY) { + public void onMouseMove(int containerX, int containerY) { + WWidget hit = screen.getDescription().getRootPanel().hit(containerX, containerY); + hovered.set(hit); + runTree( - description.getRootPanel().hit(containerX, containerY), + hit, widget -> widget.onMouseMove(containerX - widget.getAbsoluteX(), containerY - widget.getAbsoluteY()) ); } diff --git a/src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationHelper.java b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationHelper.java new file mode 100644 index 0000000..54567be --- /dev/null +++ b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationHelper.java @@ -0,0 +1,49 @@ +package io.github.cottonmc.cotton.gui.impl.client; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; +import net.minecraft.text.TranslatableText; + +import io.github.cottonmc.cotton.gui.widget.WPanel; +import io.github.cottonmc.cotton.gui.widget.WWidget; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Environment(EnvType.CLIENT) +public final class NarrationHelper { + public static void addNarrations(WPanel rootPanel, NarrationMessageBuilder builder) { + List narratableWidgets = getAllWidgets(rootPanel) + .filter(WWidget::isNarratable) + .collect(Collectors.toList()); + + for (int i = 0, childCount = narratableWidgets.size(); i < childCount; i++) { + WWidget child = narratableWidgets.get(i); + if (!child.isFocused() && !child.isHovered()) continue; + + // replicates Screen.addElementNarrations + if (narratableWidgets.size() > 1) { + builder.put(NarrationPart.POSITION, new TranslatableText(NarrationMessages.Vanilla.SCREEN_POSITION_KEY, i + 1, childCount)); + + if (child.isFocused()) { + builder.put(NarrationPart.USAGE, NarrationMessages.Vanilla.COMPONENT_LIST_USAGE); + } + } + + child.addNarrations(builder.nextMessage()); + } + } + + private static Stream getAllWidgets(WPanel panel) { + return Stream.concat(Stream.of(panel), panel.streamChildren().flatMap(widget -> { + if (widget instanceof WPanel nested) { + return getAllWidgets(nested); + } + + return Stream.of(widget); + })); + } +} diff --git a/src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationMessages.java b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationMessages.java new file mode 100644 index 0000000..bc9ee48 --- /dev/null +++ b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationMessages.java @@ -0,0 +1,29 @@ +package io.github.cottonmc.cotton.gui.impl.client; + +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; + +public final class NarrationMessages { + public static final String ITEM_SLOT_TITLE_KEY = "widget.libgui.item_slot.narration.title"; + public static final String LABELED_SLIDER_TITLE_KEY = "widget.libgui.labeled_slider.narration.title"; + public static final Text PLAYER_INVENTORY_HOTBAR = new TranslatableText("widget.libgui.player_inventory.narration.hotbar"); + public static final Text SCROLL_BAR_TITLE = new TranslatableText("widget.libgui.scroll_bar.narration.title"); + public static final String SLIDER_MESSAGE_KEY = "widget.libgui.slider.narration.title"; + public static final Text SLIDER_USAGE = new TranslatableText("widget.libgui.slider.narration.usage"); + public static final String TAB_TITLE_KEY = "widget.libgui.tab.narration.title"; + public static final String TAB_POSITION_KEY = "widget.libgui.tab.narration.position"; + public static final String TEXT_FIELD_TITLE_KEY = "widget.libgui.text_field.narration.title"; + public static final String TEXT_FIELD_SUGGESTION_KEY = "widget.libgui.text_field.narration.suggestion"; + public static final String TOGGLE_BUTTON_NAMED_KEY = "widget.libgui.toggle_button.narration.named"; + public static final Text TOGGLE_BUTTON_OFF = new TranslatableText("widget.libgui.toggle_button.narration.off"); + public static final Text TOGGLE_BUTTON_ON = new TranslatableText("widget.libgui.toggle_button.narration.on"); + public static final String TOGGLE_BUTTON_UNNAMED_KEY = "widget.libgui.toggle_button.narration.unnamed"; + + public static final class Vanilla { + public static final Text BUTTON_USAGE_FOCUSED = new TranslatableText("narration.button.usage.focused"); + public static final Text BUTTON_USAGE_HOVERED = new TranslatableText("narration.button.usage.hovered"); + public static final Text COMPONENT_LIST_USAGE = new TranslatableText("narration.component_list.usage"); + public static final Text INVENTORY = new TranslatableText("container.inventory"); + public static final String SCREEN_POSITION_KEY = "narrator.position.screen"; + } +} diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WAbstractSlider.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WAbstractSlider.java index 7fda1d9..2af6092 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WAbstractSlider.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WAbstractSlider.java @@ -2,8 +2,12 @@ package io.github.cottonmc.cotton.gui.widget; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; +import net.minecraft.text.TranslatableText; import net.minecraft.util.math.MathHelper; +import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages; import io.github.cottonmc.cotton.gui.widget.data.Axis; import io.github.cottonmc.cotton.gui.widget.data.InputResult; import org.jetbrains.annotations.Nullable; @@ -342,6 +346,13 @@ public abstract class WAbstractSlider extends WWidget { return dragging; } + @Environment(EnvType.CLIENT) + @Override + public void addNarrations(NarrationMessageBuilder builder) { + builder.put(NarrationPart.TITLE, new TranslatableText(NarrationMessages.SLIDER_MESSAGE_KEY, value, min, max)); + builder.put(NarrationPart.USAGE, NarrationMessages.SLIDER_USAGE); + } + /** * Tests if the key should decrease sliders with the specified direction. * diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WButton.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WButton.java index 15cd6bb..533e436 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WButton.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WButton.java @@ -3,6 +3,8 @@ package io.github.cottonmc.cotton.gui.widget; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; import net.minecraft.client.gui.widget.ClickableWidget; import net.minecraft.client.sound.PositionedSoundInstance; import net.minecraft.client.util.math.MatrixStack; @@ -12,6 +14,7 @@ import net.minecraft.util.Identifier; import io.github.cottonmc.cotton.gui.client.LibGui; import io.github.cottonmc.cotton.gui.client.ScreenDrawing; +import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages; import io.github.cottonmc.cotton.gui.widget.data.HorizontalAlignment; import io.github.cottonmc.cotton.gui.widget.data.InputResult; import io.github.cottonmc.cotton.gui.widget.icon.Icon; @@ -219,6 +222,20 @@ public class WButton extends WWidget { return this; } + @Environment(EnvType.CLIENT) + @Override + public void addNarrations(NarrationMessageBuilder builder) { + builder.put(NarrationPart.TITLE, ClickableWidget.getNarrationMessage(getLabel())); + + if (isEnabled()) { + if (isFocused()) { + builder.put(NarrationPart.USAGE, NarrationMessages.Vanilla.BUTTON_USAGE_FOCUSED); + } else if (isHovered()) { + builder.put(NarrationPart.USAGE, NarrationMessages.Vanilla.BUTTON_USAGE_HOVERED); + } + } + } + @Environment(EnvType.CLIENT) static Identifier getTexture() { return LibGui.isDarkMode() ? DARK_WIDGETS_LOCATION : ClickableWidget.WIDGETS_TEXTURE; diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WItemSlot.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WItemSlot.java index a99028c..a05b364 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WItemSlot.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WItemSlot.java @@ -3,16 +3,23 @@ package io.github.cottonmc.cotton.gui.widget; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.player.PlayerInventory; import net.minecraft.inventory.Inventory; import net.minecraft.item.ItemStack; import net.minecraft.screen.ScreenHandler; import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; import io.github.cottonmc.cotton.gui.GuiDescription; import io.github.cottonmc.cotton.gui.ValidatedSlot; import io.github.cottonmc.cotton.gui.client.BackgroundPainter; import io.github.cottonmc.cotton.gui.impl.VisualLogger; +import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages; +import io.github.cottonmc.cotton.gui.widget.data.InputResult; import io.github.cottonmc.cotton.gui.widget.icon.Icon; import org.jetbrains.annotations.Nullable; @@ -75,10 +82,12 @@ public class WItemSlot extends WWidget { private boolean insertingAllowed = true; private boolean takingAllowed = true; private int focusedSlot = -1; + private int hoveredSlot = -1; private Predicate filter = DEFAULT_FILTER; private final Set listeners = new HashSet<>(); public WItemSlot(Inventory inventory, int startIndex, int slotsWide, int slotsHigh, boolean big) { + this(); this.inventory = inventory; this.startIndex = startIndex; this.slotsWide = slotsWide; @@ -87,7 +96,12 @@ public class WItemSlot extends WWidget { //this.ltr = ltr; } - private WItemSlot() {} + private WItemSlot() { + hoveredProperty().addListener((property, from, to) -> { + assert to != null; + if (!to) hoveredSlot = -1; + }); + } public static WItemSlot of(Inventory inventory, int index) { WItemSlot w = new WItemSlot(); @@ -124,7 +138,12 @@ public class WItemSlot extends WWidget { * @see WPlayerInvPanel */ public static WItemSlot ofPlayerStorage(Inventory inventory) { - WItemSlot w = new WItemSlot(); + WItemSlot w = new WItemSlot() { + @Override + protected Text getNarrationName() { + return inventory instanceof PlayerInventory inv ? inv.getDisplayName() : NarrationMessages.Vanilla.INVENTORY; + } + }; w.inventory = inventory; w.startIndex = 9; w.slotsWide = 9; @@ -414,6 +433,14 @@ public class WItemSlot extends WWidget { } } + @Override + public InputResult onMouseMove(int x, int y) { + int slotX = x / 18; + int slotY = y / 18; + hoveredSlot = slotX + slotY * slotsWide; + return InputResult.PROCESSED; + } + @Override public void onHidden() { super.onHidden(); @@ -429,6 +456,34 @@ public class WItemSlot extends WWidget { backgroundPainter = BackgroundPainter.SLOT; } + @Environment(EnvType.CLIENT) + @Override + public void addNarrations(NarrationMessageBuilder builder) { + List parts = new ArrayList<>(); + Text name = getNarrationName(); + if (name != null) parts.add(name); + + if (focusedSlot >= 0) { + parts.add(new TranslatableText(NarrationMessages.ITEM_SLOT_TITLE_KEY, focusedSlot + 1, slotsWide * slotsHigh)); + } else if (hoveredSlot >= 0) { + parts.add(new TranslatableText(NarrationMessages.ITEM_SLOT_TITLE_KEY, hoveredSlot + 1, slotsWide * slotsHigh)); + } + + builder.put(NarrationPart.TITLE, parts.toArray(new Text[0])); + } + + /** + * Returns a "narration name" for this slot. + * It's narrated before the slot index. One example of a narration name would be "hotbar" for the player's hotbar. + * + * @return the narration name, or null if there's none for this slot + * @since 4.2.0 + */ + @Nullable + protected Text getNarrationName() { + return null; + } + /** * A listener for changes in an item slot. * diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabel.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabel.java index 5c6b2da..83f0700 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabel.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabel.java @@ -5,6 +5,8 @@ import net.fabricmc.api.Environment; import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.LiteralText; import net.minecraft.text.Style; @@ -261,4 +263,10 @@ public class WLabel extends WWidget { this.verticalAlignment = align; return this; } + + @Environment(EnvType.CLIENT) + @Override + public void addNarrations(NarrationMessageBuilder builder) { + builder.put(NarrationPart.TITLE, text); + } } diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabeledSlider.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabeledSlider.java index 911d547..2f972ff 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabeledSlider.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabeledSlider.java @@ -2,12 +2,16 @@ package io.github.cottonmc.cotton.gui.widget; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; import net.minecraft.util.Identifier; import net.minecraft.util.math.Vec3f; import io.github.cottonmc.cotton.gui.client.ScreenDrawing; +import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages; import io.github.cottonmc.cotton.gui.widget.data.Axis; import io.github.cottonmc.cotton.gui.widget.data.HorizontalAlignment; import org.jetbrains.annotations.Nullable; @@ -212,6 +216,17 @@ public class WLabeledSlider extends WAbstractSlider { ScreenDrawing.texturedRect(matrices, x + halfWidth, y, halfWidth, 20, texture, buttonEndLeft, buttonTop, 200 * px, buttonTop + buttonHeight, 0xFFFFFFFF); } + @Environment(EnvType.CLIENT) + @Override + public void addNarrations(NarrationMessageBuilder builder) { + if (getLabel() != null) { + builder.put(NarrationPart.TITLE, new TranslatableText(NarrationMessages.LABELED_SLIDER_TITLE_KEY, getLabel(), value, min, max)); + builder.put(NarrationPart.USAGE, NarrationMessages.SLIDER_USAGE); + } else { + super.addNarrations(builder); + } + } + /** * A label updater updates the label of a slider based on the current value. * diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WPanel.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WPanel.java index 912c1c0..204ea93 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WPanel.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WPanel.java @@ -7,6 +7,7 @@ import net.minecraft.client.util.math.MatrixStack; import io.github.cottonmc.cotton.gui.GuiDescription; import io.github.cottonmc.cotton.gui.client.BackgroundPainter; import io.github.cottonmc.cotton.gui.widget.data.Insets; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import java.util.AbstractList; @@ -247,6 +248,17 @@ public abstract class WPanel extends WWidget { } } + /** + * {@return a stream of all visible widgets in this panel} + * + * @experimental + * @since 4.2.0 + */ + @ApiStatus.Experimental + public final Stream streamChildren() { + return children.stream(); + } + @Override public String toString() { return getClass().getSimpleName() + " {\n" + children.stream().map(Object::toString).map(x -> x + ",").flatMap(x -> Stream.of(x.split("\n")).filter(y -> !y.isEmpty()).map(y -> "\t" + y)).collect(Collectors.joining("\n")) + "\n}"; diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WPlayerInvPanel.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WPlayerInvPanel.java index c105924..af4a159 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WPlayerInvPanel.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WPlayerInvPanel.java @@ -3,9 +3,11 @@ package io.github.cottonmc.cotton.gui.widget; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.text.Text; import io.github.cottonmc.cotton.gui.GuiDescription; import io.github.cottonmc.cotton.gui.client.BackgroundPainter; +import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages; import org.jetbrains.annotations.Nullable; /** @@ -54,7 +56,12 @@ public class WPlayerInvPanel extends WPlainPanel { } inv = WItemSlot.ofPlayerStorage(playerInventory); - hotbar = WItemSlot.of(playerInventory, 0, 9, 1); + hotbar = new WItemSlot(playerInventory, 0, 9, 1, false) { + @Override + protected Text getNarrationName() { + return NarrationMessages.PLAYER_INVENTORY_HOTBAR; + } + }; this.add(inv, 0, y); this.add(hotbar, 0, y + 58); } diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WScrollBar.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WScrollBar.java index 0eb7818..2882dbd 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WScrollBar.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WScrollBar.java @@ -2,10 +2,13 @@ package io.github.cottonmc.cotton.gui.widget; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; import net.minecraft.client.util.math.MatrixStack; import io.github.cottonmc.cotton.gui.client.LibGui; import io.github.cottonmc.cotton.gui.client.ScreenDrawing; +import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages; import io.github.cottonmc.cotton.gui.widget.data.Axis; import io.github.cottonmc.cotton.gui.widget.data.InputResult; @@ -264,4 +267,11 @@ public class WScrollBar extends WWidget { } if (this.value<0) this.value = 0; } + + @Environment(EnvType.CLIENT) + @Override + public void addNarrations(NarrationMessageBuilder builder) { + builder.put(NarrationPart.TITLE, NarrationMessages.SCROLL_BAR_TITLE); + builder.put(NarrationPart.USAGE, NarrationMessages.SLIDER_USAGE); + } } diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WTabPanel.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WTabPanel.java index 2ee7e92..718e44b 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WTabPanel.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WTabPanel.java @@ -4,16 +4,20 @@ import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; import net.minecraft.client.sound.PositionedSoundInstance; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.sound.SoundEvents; import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; import net.minecraft.util.Identifier; import io.github.cottonmc.cotton.gui.client.BackgroundPainter; import io.github.cottonmc.cotton.gui.client.LibGui; import io.github.cottonmc.cotton.gui.client.ScreenDrawing; import io.github.cottonmc.cotton.gui.impl.LibGuiCommon; +import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages; import io.github.cottonmc.cotton.gui.widget.data.Axis; import io.github.cottonmc.cotton.gui.widget.data.HorizontalAlignment; import io.github.cottonmc.cotton.gui.widget.data.InputResult; @@ -354,10 +358,23 @@ public class WTabPanel extends WPanel { } } + @Environment(EnvType.CLIENT) @Override public void addTooltip(TooltipBuilder tooltip) { data.addTooltip(tooltip); } + + @Environment(EnvType.CLIENT) + @Override + public void addNarrations(NarrationMessageBuilder builder) { + Text label = data.getTitle(); + + if (label != null) { + builder.put(NarrationPart.TITLE, new TranslatableText(NarrationMessages.TAB_TITLE_KEY, label)); + } + + builder.put(NarrationPart.POSITION, new TranslatableText(NarrationMessages.TAB_POSITION_KEY, tabWidgets.indexOf(this) + 1, tabWidgets.size())); + } } /** diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WText.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WText.java index 96f78ed..8a4f03e 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WText.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WText.java @@ -4,6 +4,8 @@ import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.OrderedText; import net.minecraft.text.Style; @@ -253,4 +255,10 @@ public class WText extends WWidget { this.verticalAlignment = verticalAlignment; return this; } + + @Environment(EnvType.CLIENT) + @Override + public void addNarrations(NarrationMessageBuilder builder) { + builder.put(NarrationPart.TITLE, text); + } } diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WTextField.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WTextField.java index 60ae5d4..e9e2692 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WTextField.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WTextField.java @@ -7,6 +7,8 @@ import net.fabricmc.api.Environment; import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; import net.minecraft.client.render.BufferBuilder; import net.minecraft.client.render.BufferRenderer; import net.minecraft.client.render.GameRenderer; @@ -16,11 +18,13 @@ import net.minecraft.client.render.VertexFormats; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.LiteralText; import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.Matrix4f; import io.github.cottonmc.cotton.gui.client.BackgroundPainter; import io.github.cottonmc.cotton.gui.client.ScreenDrawing; +import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages; import io.github.cottonmc.cotton.gui.widget.data.InputResult; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; @@ -431,7 +435,16 @@ public class WTextField extends WWidget { } } } - + + @Override + public void addNarrations(NarrationMessageBuilder builder) { + builder.put(NarrationPart.TITLE, new TranslatableText(NarrationMessages.TEXT_FIELD_TITLE_KEY, text)); + + if (suggestion != null) { + builder.put(NarrationPart.HINT, new TranslatableText(NarrationMessages.TEXT_FIELD_SUGGESTION_KEY, suggestion)); + } + } + /** * From an X offset past the left edge of a TextRenderer.draw, finds out what the closest caret * position (division between letters) is. diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WToggleButton.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WToggleButton.java index 8953f4d..ca0285f 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WToggleButton.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WToggleButton.java @@ -3,15 +3,19 @@ package io.github.cottonmc.cotton.gui.widget; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; import net.minecraft.client.sound.PositionedSoundInstance; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.sound.SoundEvents; import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; import net.minecraft.util.Identifier; import io.github.cottonmc.cotton.gui.client.LibGui; import io.github.cottonmc.cotton.gui.client.ScreenDrawing; import io.github.cottonmc.cotton.gui.impl.LibGuiCommon; +import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages; import io.github.cottonmc.cotton.gui.widget.data.InputResult; import io.github.cottonmc.cotton.gui.widget.data.Texture; import org.jetbrains.annotations.Nullable; @@ -202,4 +206,25 @@ public class WToggleButton extends WWidget { this.focusImage = focusImage; return this; } + + @Environment(EnvType.CLIENT) + @Override + public void addNarrations(NarrationMessageBuilder builder) { + Text onOff = isOn ? NarrationMessages.TOGGLE_BUTTON_ON : NarrationMessages.TOGGLE_BUTTON_OFF; + Text title; + + if (label != null) { + title = new TranslatableText(NarrationMessages.TOGGLE_BUTTON_NAMED_KEY, label, onOff); + } else { + title = new TranslatableText(NarrationMessages.TOGGLE_BUTTON_UNNAMED_KEY, onOff); + } + + builder.put(NarrationPart.TITLE, title); + + if (isFocused()) { + builder.put(NarrationPart.USAGE, NarrationMessages.Vanilla.BUTTON_USAGE_FOCUSED); + } else if (isHovered()) { + builder.put(NarrationPart.USAGE, NarrationMessages.Vanilla.BUTTON_USAGE_HOVERED); + } + } } diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WWidget.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WWidget.java index 2a6ae5e..27e62f8 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WWidget.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WWidget.java @@ -4,16 +4,21 @@ import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; import net.minecraft.client.util.math.MatrixStack; import io.github.cottonmc.cotton.gui.GuiDescription; import io.github.cottonmc.cotton.gui.impl.VisualLogger; import io.github.cottonmc.cotton.gui.widget.data.InputResult; +import io.github.cottonmc.cotton.gui.widget.data.ObservableProperty; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; /** * The base class for all widgets. + * + * @properties */ public class WWidget { private static final VisualLogger LOGGER = new VisualLogger(WWidget.class); @@ -41,6 +46,8 @@ public class WWidget { @Nullable protected GuiDescription host; + private final ObservableProperty hovered = ObservableProperty.of(false).nonnullValues().setName("WWidget.hovered"); + /** * Sets the location of this widget relative to its parent. * @@ -456,6 +463,68 @@ public class WWidget { public void addPainters() { } + /** + * Returns whether the user is hovering over this widget. + * The result is an observable property that can be modified and listened to. + * + * @experimental + * @return the {@code hovered} property + * @since 4.2.0 + */ + @ApiStatus.Experimental + public ObservableProperty hoveredProperty() { + return hovered; + } + + /** + * Returns whether the user is hovering over this widget. + * This is equivalent to calling {@link #hoveredProperty()}.get(). + * + * @experimental + * @return true if this widget is hovered, false otherwise + * @since 4.2.0 + */ + @ApiStatus.Experimental + public final boolean isHovered() { + return hoveredProperty().get(); + } + + /** + * Sets the {@link #hoveredProperty() hovered} property. + * + * @experimental + * @param hovered the new value; true if hovered, false otherwise + * @since 4.2.0 + */ + @ApiStatus.Experimental + public final void setHovered(boolean hovered) { + hoveredProperty().set(hovered); + } + + /** + * {@return whether this widget can be narrated} + * + * @see #addNarrations(NarrationMessageBuilder) + * @since 4.2.0 + */ + public boolean isNarratable() { + return true; + } + + /** + * Adds the narrations of this widget to a narration builder. + * Narrations will only apply if this widget {@linkplain #isNarratable() is narratable}. + * + *

As of LibGui 4.2.0, the widget also needs to be {@linkplain #canFocus() focusable}, but that is + * planned to be changed in the future to include "hoverable" widgets. + * + * @param builder the narration builder, cannot be null + * @since 4.2.0 + */ + @Environment(EnvType.CLIENT) + public void addNarrations(NarrationMessageBuilder builder) { + } + /** * Tests if the provided key code is an activation key for widgets. * diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableProperty.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableProperty.java new file mode 100644 index 0000000..eb92387 --- /dev/null +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableProperty.java @@ -0,0 +1,162 @@ +package io.github.cottonmc.cotton.gui.widget.data; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * An observable mutable property. Observable properties are containers for values + * that can be modified and listened to. + * + *

The naming convention for {@code ObservableProperty} getters follows the convention + * {@code Property}. For example, the {@code WWidget.hovered} property can be retrieved with + * {@link io.github.cottonmc.cotton.gui.widget.WWidget#hoveredProperty() hoveredProperty()}. + * + * @experimental + * @param the contained value type + * @since 4.2.0 + */ +@ApiStatus.Experimental +public final class ObservableProperty implements ObservableView { + private boolean hasValue; + private T value; + private final List> listeners = new ArrayList<>(); + private boolean allowNull = true; + private String name = ""; + + private ObservableProperty(@Nullable T value, boolean hasValue) { + this.value = value; + this.hasValue = hasValue; + } + + public static ObservableProperty lateinit() { + return new ObservableProperty<>(null, false); + } + + public static ObservableProperty of(T initialValue) { + return new ObservableProperty<>(initialValue, true); + } + + @Override + public boolean hasValue() { + return hasValue; + } + + @Override + public T get() { + if (!hasValue) { + throw new IllegalStateException("Property " + name + " not initialized!"); + } + + return value; + } + + /** + * Sets this property to a constant value. + * + * @param value the new value + * @throws NullPointerException if the value is null and nulls aren't allowed + */ + public void set(T value) { + if (value == null && !allowNull) throw new NullPointerException("Trying to set null value for nonnull property " + name); + T oldValue = this.value; + this.value = value; + hasValue = true; + + if (oldValue != value) { + for (ChangeListener listener : listeners) { + listener.onPropertyChange(this, oldValue, value); + } + } + } + + /** + * Clears the current value, if any, from this property. + */ + public void clear() { + T oldValue = value; + value = null; + hasValue = false; + + if (oldValue != null) { + for (ChangeListener listener : listeners) { + listener.onPropertyChange(this, oldValue, null); + } + } + } + + /** + * Prevents this property from accepting null values. + * + * @return this property + */ + public ObservableProperty nonnullValues() { + allowNull = false; + return this; + } + + /** + * Returns a read-only view of this property. + * The result is not an instance of {@link ObservableProperty}, + * and thus can't be mutated. + * + * @return an observable view of this property + */ + public ObservableView readOnly() { + // Missing delegates from Kotlin... :( + return new ObservableView<>() { + @Override + public boolean hasValue() { + return ObservableProperty.this.hasValue(); + } + + @Override + public T get() { + return ObservableProperty.this.get(); + } + + @Override + public void addListener(ChangeListener listener) { + ObservableProperty.this.addListener(listener); + } + + @Override + public void removeListener(ChangeListener listener) { + ObservableProperty.this.removeListener(listener); + } + }; + } + + /** + * {@return the name of this property} + */ + public String getName() { + return name; + } + + /** + * Sets the name of this property, which is used in debug messages. + * + * @param name the new name + * @return this property + */ + public ObservableProperty setName(String name) { + this.name = Objects.requireNonNull(name, "name"); + return this; + } + + @Override + public void addListener(ChangeListener listener) { + Objects.requireNonNull(listener); + listeners.add(listener); + } + + @Override + public void removeListener(ChangeListener listener) { + Objects.requireNonNull(listener); + listeners.remove(listener); + } +} diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableView.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableView.java new file mode 100644 index 0000000..5f6a33c --- /dev/null +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableView.java @@ -0,0 +1,75 @@ +package io.github.cottonmc.cotton.gui.widget.data; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; +import java.util.function.Supplier; + +/** + * A read-only {@linkplain ObservableProperty observable property}. + * + * @experimental + * @param the contained value type + * @since 4.2.0 + */ +@ApiStatus.Experimental +public interface ObservableView extends Supplier { + /** + * {@return whether this property has been set to a value} + */ + boolean hasValue(); + + /** + * {@return the value of this property} + * @throws IllegalStateException if not initialized + * @see #hasValue() + */ + @Override + T get(); + + /** + * {@return the value of this property, or null if not initialized} + */ + default @Nullable T getOrNull() { + return hasValue() ? get() : null; + } + + /** + * {@return the nonnull value of this property, or {@link Optional#empty()} if null or not initialized} + */ + default Optional find() { + return Optional.ofNullable(getOrNull()); + } + + /** + * Adds a change listener to this property view. + * + * @param listener the added listener + */ + void addListener(ChangeListener listener); + + /** + * Removes a change listener from this property view if present. + * + * @param listener the removed listener + */ + void removeListener(ChangeListener listener); + + /** + * A listener for changes in observable views and properties. + * + * @param the value type listened to + */ + @FunctionalInterface + interface ChangeListener { + /** + * Handles a change in an observable property. + * + * @param property the changed property or view + * @param from the previous value, or null if not set before + * @param to the new value, or null if cleared + */ + void onPropertyChange(ObservableView property, @Nullable T from, @Nullable T to); + } +} -- cgit