diff options
author | Juuxel <6596629+Juuxel@users.noreply.github.com> | 2021-09-11 01:24:34 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-11 01:24:34 +0300 |
commit | 60283b69816b6792ab517089707cfe4d4a099e9a (patch) | |
tree | beaf7f0db6491158fda39b32a0f3e0fefa46edbe | |
parent | b8628421fb13f30853cdc3b3800b7153b168b65f (diff) | |
download | LibGui-60283b69816b6792ab517089707cfe4d4a099e9a.tar.gz LibGui-60283b69816b6792ab517089707cfe4d4a099e9a.tar.bz2 LibGui-60283b69816b6792ab517089707cfe4d4a099e9a.zip |
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
27 files changed, 893 insertions, 30 deletions
diff --git a/build.gradle b/build.gradle index 9f04998..4e6d927 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,10 @@ archivesBaseName = project.archives_base_name version = "$project.mod_version+$project.minecraft_version" group = project.maven_group +configurations { + javadocClasspath +} + repositories { maven { url "https://server.bbkr.space/artifactory/libs-release" } /*maven { @@ -47,6 +51,8 @@ dependencies { modRuntime(modCompileOnly("com.terraformersmc:modmenu:$project.modmenu_version") { exclude group: 'net.fabricmc.fabric-api' }) + + javadocClasspath project(':javadoc') } processResources { @@ -79,9 +85,16 @@ checkstyle { toolVersion = '8.36.2' } +evaluationDependsOn(':javadoc') + javadoc { + dependsOn project(':javadoc').tasks.jar + options { links("https://maven.fabricmc.net/docs/yarn-$project.yarn_mappings") + taglets 'io.github.cottonmc.cotton.gui.jd.ExperimentalTaglet' + taglets 'io.github.cottonmc.cotton.gui.jd.PropertyTaglet' + tagletPath project(':javadoc').tasks.jar.outputs.files.singleFile } exclude("**/impl/**") diff --git a/javadoc/build.gradle b/javadoc/build.gradle new file mode 100644 index 0000000..e8300bb --- /dev/null +++ b/javadoc/build.gradle @@ -0,0 +1,6 @@ +plugins { + id 'java' +} + +sourceCompatibility = rootProject.sourceCompatibility +targetCompatibility = rootProject.targetCompatibility diff --git a/javadoc/src/main/java/io/github/cottonmc/cotton/gui/jd/ExperimentalTaglet.java b/javadoc/src/main/java/io/github/cottonmc/cotton/gui/jd/ExperimentalTaglet.java new file mode 100644 index 0000000..dbd2391 --- /dev/null +++ b/javadoc/src/main/java/io/github/cottonmc/cotton/gui/jd/ExperimentalTaglet.java @@ -0,0 +1,30 @@ +package io.github.cottonmc.cotton.gui.jd; + +import com.sun.source.doctree.DocTree; +import jdk.javadoc.doclet.Taglet; + +import java.util.List; +import java.util.Set; +import javax.lang.model.element.Element; + +public class ExperimentalTaglet implements Taglet { + @Override + public Set<Location> getAllowedLocations() { + return Set.of(Location.values()); + } + + @Override + public boolean isInlineTag() { + return false; + } + + @Override + public String getName() { + return "experimental"; + } + + @Override + public String toString(List<? extends DocTree> tags, Element element) { + return "<dt>Experimental API:</dt><dd>Might be modified or removed without prior notice until stabilised.</dd>"; + } +} diff --git a/javadoc/src/main/java/io/github/cottonmc/cotton/gui/jd/PropertyTaglet.java b/javadoc/src/main/java/io/github/cottonmc/cotton/gui/jd/PropertyTaglet.java new file mode 100644 index 0000000..163f1d6 --- /dev/null +++ b/javadoc/src/main/java/io/github/cottonmc/cotton/gui/jd/PropertyTaglet.java @@ -0,0 +1,174 @@ +package io.github.cottonmc.cotton.gui.jd; + +import com.sun.source.doctree.DocTree; +import com.sun.source.doctree.TextTree; +import com.sun.source.util.DocTrees; +import com.sun.source.util.SimpleDocTreeVisitor; +import jdk.javadoc.doclet.Doclet; +import jdk.javadoc.doclet.DocletEnvironment; +import jdk.javadoc.doclet.Taglet; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeVisitor; +import javax.lang.model.util.ElementKindVisitor8; +import javax.lang.model.util.SimpleTypeVisitor8; + +public class PropertyTaglet implements Taglet { + private static final Pattern PROPERTY_METHOD = Pattern.compile("^(.+)Property$"); + private static final String OBSERVABLE_PROPERTY = "io.github.cottonmc.cotton.gui.widget.data.ObservableProperty"; + private DocTrees docTrees; + + @Override + public void init(DocletEnvironment env, Doclet doclet) { + docTrees = env.getDocTrees(); + } + + @Override + public Set<Location> getAllowedLocations() { + return Set.of(Location.TYPE); + } + + @Override + public boolean isInlineTag() { + return false; + } + + @Override + public String getName() { + return "properties"; + } + + @Override + public String toString(List<? extends DocTree> tags, Element element) { + if (!((element.getKind().isClass() || element.getKind().isInterface()) && element instanceof TypeElement type)) { + throw new IllegalArgumentException("Not a type: " + element); + } + + List<PropertyEntry> myEntries = scan(type); + StringBuilder builder = new StringBuilder(); + + if (!myEntries.isEmpty()) { + builder.append("<div class=\"caption\"><span>Properties</span></div>"); + builder.append("<div class=\"summary-table two-column-summary\">"); + builder.append("<div class=\"table-header col-first\">Property</div>"); + builder.append("<div class=\"table-header col-last\">Description</div>"); + + for (int i = 0; i < myEntries.size(); i++) { + PropertyEntry entry = myEntries.get(i); + String rowClass = (i % 2 == 0) ? "even-row-color" : "odd-row-color"; + builder.append("<div class=\"col-first ").append(rowClass).append("\"><code><span class=\"member-name-link\">"); + builder.append("<a href=\"#").append(entry.name).append("Property()\">").append(entry.name).append("</a>"); + builder.append("</span></code></div>"); + builder.append("<div class=\"col-last ").append(rowClass).append("\">").append(entry.doc).append("</div>"); + } + + builder.append("</div>"); + } + + Map<String, List<PropertyEntry>> inheritedProperties = new LinkedHashMap<>(); + scanParents(type, inheritedProperties); + + inheritedProperties.forEach((name, entries) -> { + if (!entries.isEmpty()) { + builder.append("<dt>Properties from <code>").append(name).append("</code></dt>"); + builder.append("<dd>"); + builder.append(entries.stream().map(entry -> "<code>" + entry.name + "</code>").collect(Collectors.joining(", "))); + builder.append("</dd>"); + } + }); + + return builder.toString(); + } + + private static String getClassName(TypeElement cl) { + return cl.getQualifiedName().toString(); + } + + private List<PropertyEntry> scan(TypeElement cl) { + // Java classes don't have LibGui properties (yet? 😳) + if (getClassName(cl).startsWith("java.")) return List.of(); + + return cl.getEnclosedElements().stream() + .filter(el -> el.getKind() == ElementKind.METHOD) + .map(el -> (ExecutableElement) el) + .filter(el -> el.getReturnType().accept(new ObservableTypeFilter(), null)) + .map(el -> { + Matcher matcher = PROPERTY_METHOD.matcher(el.getSimpleName()); + + if (matcher.matches()) { + String doc = docTrees.getDocCommentTree(el).getFirstSentence().stream() + .map(tree -> tree.accept(new SimpleDocTreeVisitor<String, Void>() { + @Override + public String visitText(TextTree node, Void o) { + return node.getBody(); + } + }, null)) + .filter(Objects::nonNull) + .findAny().orElse(""); + + return new PropertyEntry(matcher.group(1), doc); + } + + return null; + }) + .filter(Objects::nonNull) + .sorted() + .collect(Collectors.toList()); + } + + private void scanParents(TypeElement cl, Map<String, List<PropertyEntry>> inheritedProperties) { + TypeVisitor<Void, Void> typeVisitor = new SimpleTypeVisitor8<>() { + @Override + public Void visitDeclared(DeclaredType t, Void o) { + return t.asElement().accept(new ElementKindVisitor8<>() { + @Override + public Void visitType(TypeElement e, Object o) { + String fqn = e.getQualifiedName().toString(); + inheritedProperties.put(fqn, scan(e)); + scanParents(e, inheritedProperties); + return null; + } + }, null); + } + }; + + cl.getSuperclass().accept(typeVisitor, null); + cl.getInterfaces().forEach(itf -> itf.accept(typeVisitor, null)); + } + + private static final class ObservableTypeFilter extends SimpleTypeVisitor8<Boolean, Void> { + ObservableTypeFilter() { + super(false); + } + + @Override + public Boolean visitDeclared(DeclaredType t, Void v) { + Element type = t.asElement(); + + if (type.getKind() == ElementKind.CLASS) { + return ((TypeElement) type).getQualifiedName().contentEquals(OBSERVABLE_PROPERTY); + } + + return false; + } + } + + private record PropertyEntry(String name, String doc) implements Comparable<PropertyEntry> { + @Override + public int compareTo(PropertyEntry o) { + return name.compareTo(o.name); + } + } +} diff --git a/settings.gradle b/settings.gradle index 1dfe6ed..4e731fa 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,3 +15,4 @@ pluginManagement { rootProject.name = 'LibGui' include ':GuiTest' +include 'javadoc' 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<CottonClientScreen> 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<T extends SyncedGuiDescription> extends HandledScreen<T> implements CottonScreenImpl { protected SyncedGuiDescription description; @Nullable protected WWidget lastResponder = null; + private final MouseInputHandler<CottonInventoryScreen<T>> mouseInputHandler = new MouseInputHandler<>(this); /** * Constructs a new screen without a title. @@ -83,6 +88,12 @@ public class CottonInventoryScreen<T extends SyncedGuiDescription> extends Handl VisualLogger.reset(); } + @ApiStatus.Internal + @Override + public GuiDescription getDescription() { + return description; + } + @Nullable @Override public WWidget getLastResponder() { @@ -187,7 +198,7 @@ public class CottonInventoryScreen<T extends SyncedGuiDescription> 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<T extends SyncedGuiDescription> 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<T extends SyncedGuiDescription> 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<T extends SyncedGuiDescription> 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<T extends SyncedGuiDescription> 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<T extends SyncedGuiDescription> 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<S extends Screen & CottonScreenImpl> { + private final S screen; + private final ObservableProperty<WWidget> 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 <S extends Screen & CottonScreenImpl> 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 <S extends Screen & CottonScreenImpl> 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<WWidget> 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<WWidget> 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; @@ -220,6 +223,20 @@ public class WButton extends WWidget { } @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<ItemStack> filter = DEFAULT_FILTER; private final Set<ChangeListener> 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; @@ -415,6 +434,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<Text> 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<WWidget> 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<Boolean> hovered = ObservableProperty.of(false).nonnullValues().setName("WWidget.hovered"); + /** * Sets the location of this widget relative to its parent. * @@ -457,6 +464,68 @@ public class WWidget { } /** + * Returns whether the user is hovering over this widget. + * The result is an <i>observable property</i> that can be modified and listened to. + * + * @experimental + * @return the {@code hovered} property + * @since 4.2.0 + */ + @ApiStatus.Experimental + public ObservableProperty<Boolean> hoveredProperty() { + return hovered; + } + + /** + * Returns whether the user is hovering over this widget. + * This is equivalent to calling <code>{@link #hoveredProperty()}.get()</code>. + * + * @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}. + * + * <p>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. * * <p>The activation keys are Enter, keypad Enter, and Space. 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. + * + * <p>The naming convention for {@code ObservableProperty} getters follows the convention + * {@code <property name>Property}. For example, the {@code WWidget.hovered} property can be retrieved with + * {@link io.github.cottonmc.cotton.gui.widget.WWidget#hoveredProperty() hoveredProperty()}. + * + * @experimental + * @param <T> the contained value type + * @since 4.2.0 + */ +@ApiStatus.Experimental +public final class ObservableProperty<T> implements ObservableView<T> { + private boolean hasValue; + private T value; + private final List<ChangeListener<? super T>> listeners = new ArrayList<>(); + private boolean allowNull = true; + private String name = "<unnamed>"; + + private ObservableProperty(@Nullable T value, boolean hasValue) { + this.value = value; + this.hasValue = hasValue; + } + + public static <T> ObservableProperty<T> lateinit() { + return new ObservableProperty<>(null, false); + } + + public static <T> ObservableProperty<T> 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<? super T> 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<? super T> listener : listeners) { + listener.onPropertyChange(this, oldValue, null); + } + } + } + + /** + * Prevents this property from accepting null values. + * + * @return this property + */ + public ObservableProperty<T> 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<T> 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<? super T> listener) { + ObservableProperty.this.addListener(listener); + } + + @Override + public void removeListener(ChangeListener<? super T> 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<T> setName(String name) { + this.name = Objects.requireNonNull(name, "name"); + return this; + } + + @Override + public void addListener(ChangeListener<? super T> listener) { + Objects.requireNonNull(listener); + listeners.add(listener); + } + + @Override + public void removeListener(ChangeListener<? super T> 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 <T> the contained value type + * @since 4.2.0 + */ +@ApiStatus.Experimental +public interface ObservableView<T> extends Supplier<T> { + /** + * {@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<T> find() { + return Optional.ofNullable(getOrNull()); + } + + /** + * Adds a change listener to this property view. + * + * @param listener the added listener + */ + void addListener(ChangeListener<? super T> listener); + + /** + * Removes a change listener from this property view if present. + * + * @param listener the removed listener + */ + void removeListener(ChangeListener<? super T> listener); + + /** + * A listener for changes in observable views and properties. + * + * @param <T> the value type listened to + */ + @FunctionalInterface + interface ChangeListener<T> { + /** + * 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<? extends T> property, @Nullable T from, @Nullable T to); + } +} diff --git a/src/main/resources/assets/libgui/lang/en_us.json b/src/main/resources/assets/libgui/lang/en_us.json index 5ed8bfb..4f0f9a1 100644 --- a/src/main/resources/assets/libgui/lang/en_us.json +++ b/src/main/resources/assets/libgui/lang/en_us.json @@ -1,4 +1,18 @@ { - "options.libgui.libgui_settings": "LibGui Settings", - "option.libgui.darkmode": "Dark Mode" -} + "options.libgui.libgui_settings": "LibGui Settings", + "option.libgui.darkmode": "Dark Mode", + "widget.libgui.item_slot.narration.title": "Item slot %s out of %s", + "widget.libgui.labeled_slider.narration.title": "%s slider: at %s between %s and %s", + "widget.libgui.player_inventory.narration.hotbar": "Hotbar", + "widget.libgui.scroll_bar.narration.title": "Scroll bar", + "widget.libgui.slider.narration.title": "Slider: at %s between %s and %s", + "widget.libgui.slider.narration.usage": "Move by dragging or with arrow keys", + "widget.libgui.tab.narration.title": "%s tab", + "widget.libgui.tab.narration.position": "Tab %s out of %s", + "widget.libgui.text_field.narration.title": "Text field: %s", + "widget.libgui.text_field.narration.suggestion": "Suggestion: %s", + "widget.libgui.toggle_button.narration.unnamed": "Toggle button: %s", + "widget.libgui.toggle_button.narration.named": "Toggle button: %s (%s)", + "widget.libgui.toggle_button.narration.on": "On", + "widget.libgui.toggle_button.narration.off": "Off" +} |