From c8ca2e46ef4c4357e7c0d6c8cb257d58450e5de8 Mon Sep 17 00:00:00 2001 From: isxander Date: Wed, 6 Aug 2025 00:19:14 +0100 Subject: [PATCH 01/12] screen keyboard v2 - data driven layouts! --- .../dev/isxander/controlify/Controlify.java | 8 + .../isxander/controlify/api/guide/Rule.java | 2 +- .../controlify/bindings/input/InputType.java | 8 +- .../controller/id/ControllerTypeManager.java | 11 +- .../AbstractSignEditScreenMixin.java | 19 + .../screenkeyboard/ChatScreenMixin.java | 28 +- .../screenkeyboard/ChatKeyboardWidget.java | 90 ----- .../screenkeyboard/KeyPressConsumer.java | 29 -- .../controlify/screenkeyboard/KeyWidget.java | 161 +++++++++ .../screenkeyboard/KeyboardInputConsumer.java | 7 + .../screenkeyboard/KeyboardLayout.java | 198 ++++++++++ .../screenkeyboard/KeyboardLayoutManager.java | 89 +++++ .../screenkeyboard/KeyboardLayouts.java | 16 + .../screenkeyboard/KeyboardWidget.java | 342 +++--------------- .../utils/{ => codec}/FuzzyMapCodec.java | 2 +- .../utils/{ => codec}/SetCodec.java | 2 +- .../{ => codec}/StrictEitherMapCodec.java | 2 +- .../controlify/keyboard_layout/chat.json | 8 + src/main/resources/controlify.mixins.json | 1 + 19 files changed, 588 insertions(+), 435 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/AbstractSignEditScreenMixin.java delete mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/ChatKeyboardWidget.java delete mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/KeyPressConsumer.java create mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java create mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardInputConsumer.java create mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java create mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutManager.java create mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayouts.java rename src/main/java/dev/isxander/controlify/utils/{ => codec}/FuzzyMapCodec.java (96%) rename src/main/java/dev/isxander/controlify/utils/{ => codec}/SetCodec.java (98%) rename src/main/java/dev/isxander/controlify/utils/{ => codec}/StrictEitherMapCodec.java (96%) create mode 100644 src/main/resources/assets/controlify/keyboard_layout/chat.json diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index ddbee202..30dafb7f 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -36,6 +36,7 @@ import dev.isxander.controlify.platform.main.PlatformMainUtil; import dev.isxander.controlify.platform.network.SidedNetworkApi; import dev.isxander.controlify.rumble.RumbleManager; +import dev.isxander.controlify.screenkeyboard.KeyboardLayoutManager; import dev.isxander.controlify.server.*; import dev.isxander.controlify.screenop.ScreenProcessorProvider; import dev.isxander.controlify.config.ControlifyConfig; @@ -81,6 +82,7 @@ public class Controlify implements ControlifyApi { private InputFontMapper inputFontMapper; private DefaultBindManager defaultBindManager; private ControllerTypeManager controllerTypeManager; + private KeyboardLayoutManager keyboardLayoutManager; private Set thisTickContexts; private ControllerHIDService controllerHIDService; @@ -116,9 +118,11 @@ public void preInitialiseControlify() { this.inputFontMapper = new InputFontMapper(); this.defaultBindManager = new DefaultBindManager(); this.controllerTypeManager = new ControllerTypeManager(); + this.keyboardLayoutManager = new KeyboardLayoutManager(); PlatformClientUtil.registerAssetReloadListener(inputFontMapper); PlatformClientUtil.registerAssetReloadListener(defaultBindManager); PlatformClientUtil.registerAssetReloadListener(controllerTypeManager); + PlatformClientUtil.registerAssetReloadListener(keyboardLayoutManager); PlatformClientUtil.registerAssetReloadListener(GuideDomains.IN_GAME); PlatformClientUtil.registerAssetReloadListener(GuideDomains.CONTAINER); @@ -682,6 +686,10 @@ public ControllerTypeManager controllerTypeManager() { return controllerTypeManager; } + public KeyboardLayoutManager keyboardLayoutManager() { + return keyboardLayoutManager; + } + public Set thisTickBindContexts() { return this.thisTickContexts; } diff --git a/src/main/java/dev/isxander/controlify/api/guide/Rule.java b/src/main/java/dev/isxander/controlify/api/guide/Rule.java index c48afbfc..24ecb9c1 100644 --- a/src/main/java/dev/isxander/controlify/api/guide/Rule.java +++ b/src/main/java/dev/isxander/controlify/api/guide/Rule.java @@ -3,7 +3,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import dev.isxander.controlify.api.bind.InputBindingSupplier; -import dev.isxander.controlify.utils.SetCodec; +import dev.isxander.controlify.utils.codec.SetCodec; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentSerialization; import net.minecraft.resources.ResourceLocation; diff --git a/src/main/java/dev/isxander/controlify/bindings/input/InputType.java b/src/main/java/dev/isxander/controlify/bindings/input/InputType.java index 7726c3f2..5c1fc384 100644 --- a/src/main/java/dev/isxander/controlify/bindings/input/InputType.java +++ b/src/main/java/dev/isxander/controlify/bindings/input/InputType.java @@ -1,21 +1,17 @@ package dev.isxander.controlify.bindings.input; import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; import dev.isxander.controlify.utils.CUtil; -import dev.isxander.controlify.utils.FuzzyMapCodec; -import dev.isxander.controlify.utils.StrictEitherMapCodec; +import dev.isxander.controlify.utils.codec.FuzzyMapCodec; +import dev.isxander.controlify.utils.codec.StrictEitherMapCodec; import net.minecraft.Util; import net.minecraft.util.ExtraCodecs; import net.minecraft.util.StringRepresentable; import org.jetbrains.annotations.NotNull; import java.util.Arrays; -import java.util.Optional; import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; public record InputType(String id, MapCodec codec) implements StringRepresentable { diff --git a/src/main/java/dev/isxander/controlify/controller/id/ControllerTypeManager.java b/src/main/java/dev/isxander/controlify/controller/id/ControllerTypeManager.java index cdcdd7b3..d1c82e3c 100644 --- a/src/main/java/dev/isxander/controlify/controller/id/ControllerTypeManager.java +++ b/src/main/java/dev/isxander/controlify/controller/id/ControllerTypeManager.java @@ -51,20 +51,19 @@ public Map getTypeMap() { @Override public CompletableFuture load(ResourceManager manager, Executor executor) { return CompletableFuture.supplyAsync(() -> manager.getResourceStack(CUtil.rl("controllers/controller_identification.json5")), executor) - .thenComposeAsync(resources -> { + .thenCompose(resources -> { List>>> futures = new ArrayList<>(); for (Resource resource : resources) { futures.add(CompletableFuture.supplyAsync(() -> readIdentificationResource(resource), executor)); } return Util.sequence(futures) - .thenApplyAsync(listOfEntries -> listOfEntries.stream() + .thenApply(listOfEntries -> listOfEntries.stream() .flatMap(List::stream) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> b)), - executor); + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> b))); - }, executor) - .thenApplyAsync(Preparations::new, executor); + }) + .thenApply(Preparations::new); } private List> readIdentificationResource(Resource resource) { diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/AbstractSignEditScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/AbstractSignEditScreenMixin.java new file mode 100644 index 00000000..6ddca044 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/AbstractSignEditScreenMixin.java @@ -0,0 +1,19 @@ +package dev.isxander.controlify.mixins.feature.screenkeyboard; + +import dev.isxander.controlify.screenkeyboard.KeyboardWidget; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractSignEditScreen; +import net.minecraft.network.chat.Component; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(AbstractSignEditScreen.class) +public abstract class AbstractSignEditScreenMixin extends Screen { + + @Unique + private KeyboardWidget keyboard; + + protected AbstractSignEditScreenMixin(Component title) { + super(title); + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java index fba917e5..fc70ae01 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java @@ -4,8 +4,9 @@ import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.controller.keyboard.NativeKeyboardComponent; import dev.isxander.controlify.screenkeyboard.ChatKeyboardDucky; -import dev.isxander.controlify.screenkeyboard.ChatKeyboardWidget; -import dev.isxander.controlify.screenkeyboard.KeyPressConsumer; +import dev.isxander.controlify.screenkeyboard.KeyboardInputConsumer; +import dev.isxander.controlify.screenkeyboard.KeyboardLayouts; +import dev.isxander.controlify.screenkeyboard.KeyboardWidget; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.screens.ChatScreen; import net.minecraft.client.gui.screens.Screen; @@ -23,7 +24,7 @@ @Mixin(ChatScreen.class) public abstract class ChatScreenMixin extends Screen implements ChatKeyboardDucky { @Unique - private ChatKeyboardWidget keyboard; + private KeyboardWidget keyboard; @Unique private float shiftChatAmt = 0f; @@ -52,16 +53,17 @@ private void addKeyboard(CallbackInfo ci) { } else { this.shiftChatAmt = 0.5f; int keyboardHeight = (int) (this.height * this.shiftChatAmt); - this.addRenderableWidget(keyboard = new ChatKeyboardWidget((ChatScreen) (Object) this, 0, this.height - keyboardHeight, this.width, keyboardHeight, KeyPressConsumer.of( - (keycode, scancode, modifiers) -> { - input.keyPressed(keycode, scancode, modifiers); - this.keyPressed(keycode, scancode, modifiers); - }, - (codePoint, modifiers) -> { - this.charTyped(codePoint, modifiers); - input.charTyped(codePoint, modifiers); - } - ))); + this.addRenderableWidget(keyboard = new KeyboardWidget(0, this.height - keyboardHeight, this.width, keyboardHeight, KeyboardLayouts.chat(), new KeyboardInputConsumer() { + @Override + public void acceptChar(char ch, int modifiers) { + input.charTyped(ch, modifiers); + } + + @Override + public void acceptKeyCode(int keycode, int scancode, int modifiers) { + input.keyPressed(keycode, scancode, modifiers); + } + }, (ChatScreen) (Object) this)); } }); } diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/ChatKeyboardWidget.java b/src/main/java/dev/isxander/controlify/screenkeyboard/ChatKeyboardWidget.java deleted file mode 100644 index 3311b8ab..00000000 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/ChatKeyboardWidget.java +++ /dev/null @@ -1,90 +0,0 @@ -package dev.isxander.controlify.screenkeyboard; - -import com.mojang.blaze3d.platform.InputConstants; -import dev.isxander.controlify.bindings.ControlifyBindings; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; -import org.lwjgl.glfw.GLFW; - -public class ChatKeyboardWidget extends KeyboardWidget { - - public ChatKeyboardWidget(Screen screen, int x, int y, int width, int height, KeyPressConsumer keyPressConsumer) { - super(screen, x, y, width, height, keyPressConsumer); - } - - @Override - protected void arrangeKeys() { - var builder = new KeyLayoutBuilder<>(14, 5, this); - - builder.key(Key.builder(KeyFunction.ofRegularKey(InputConstants.KEY_ESCAPE, "Esc"), null), 1f); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_1, '1'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_2, '2'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_3, '3'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_4, '4'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_5, '5'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_6, '6'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_7, '7'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_8, '8'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_9, '9'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_0, '0'), null), 1); - builder.key(Key.builder(KeyFunction.ofRegularKey(InputConstants.KEY_BACKSPACE, "Backspace"), ControlifyBindings.GUI_ABSTRACT_ACTION_1), 3f); - - builder.nextRow(); - - builder.key(Key.builder(KeyFunction.ofRegularKey(InputConstants.KEY_TAB, "Tab"), null), 2f); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_Q, 'q'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_W, 'w'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_E, 'e'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_R, 'r'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_T, 't'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_Y, 'y'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_U, 'u'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_I, 'i'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_O, 'o'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_P, 'p'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_BACKSLASH, '\\'), null), 2f); - - builder.nextRow(); - - builder.key(Key.builder(KeyFunction.ofRegularKey(InputConstants.KEY_CAPSLOCK, "Caps"), null), 2f); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_A, 'a'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_S, 's'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_D, 'd'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_F, 'f'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_G, 'g'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_H, 'h'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_J, 'j'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_K, 'k'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_L, 'l'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_APOSTROPHE, '\'', 0, InputConstants.KEY_APOSTROPHE, '"', GLFW.GLFW_MOD_SHIFT), null), 1); - builder.key(Key.builder(KeyFunction.ofRegularKey(InputConstants.KEY_RETURN, "Enter"), ControlifyBindings.GUI_ABSTRACT_ACTION_2), 2f); - - builder.nextRow(); - - builder.key(Key.builder(new KeyFunction((screen, key) -> { - shiftMode = !shiftMode; - key.setHighlighted(shiftMode); - }, Key.ForegroundRenderer.text(Component.literal("Shift"))).copyShifted(), ControlifyBindings.GUI_ABSTRACT_ACTION_3), 2f); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_Z, 'z'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_X, 'x'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_C,'c'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_V, 'v'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_B, 'b'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_N, 'n'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_M, 'm'), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_COMMA, ',', 0, InputConstants.KEY_PERIOD, '.', 0), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_1, '!', GLFW.GLFW_MOD_SHIFT, InputConstants.KEY_SLASH, '?', GLFW.GLFW_MOD_SHIFT), null), 1); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_SLASH, '/', 0, InputConstants.KEY_BACKSLASH, '\\', 0), null), 1); - builder.key(Key.builder(KeyFunction.ofRegularKey(InputConstants.KEY_UP, "\u2191"), null), 1f); - - builder.nextRow(); - - builder.key(Key.builder(KeyFunction.ofRegularKey(InputConstants.KEY_LCONTROL, "Ctrl"), null), 2f); - builder.key(Key.builder(KeyFunction.ofChar(InputConstants.KEY_SPACE, ' '), null), 9f); - builder.key(Key.builder(KeyFunction.ofRegularKey(InputConstants.KEY_LEFT, "\u2190"), null), 1f); - builder.key(Key.builder(KeyFunction.ofRegularKey(InputConstants.KEY_DOWN, "\u2193"), null), 1f); - builder.key(Key.builder(KeyFunction.ofRegularKey(InputConstants.KEY_RIGHT, "\u2192"), null), 1f); - - builder.build(keys::add); - } -} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyPressConsumer.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyPressConsumer.java deleted file mode 100644 index 2cdd1f6f..00000000 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyPressConsumer.java +++ /dev/null @@ -1,29 +0,0 @@ -package dev.isxander.controlify.screenkeyboard; - -public interface KeyPressConsumer { - void acceptKeyCode(int keycode, int scancode, int modifiers); - - void acceptChar(char codePoint, int modifiers); - - static KeyPressConsumer of(KeyCodeConsumer keyCodeConsumer, CharConsumer charConsumer) { - return new KeyPressConsumer() { - @Override - public void acceptKeyCode(int keycode, int scancode, int modifiers) { - keyCodeConsumer.accept(keycode, scancode, modifiers); - } - - @Override - public void acceptChar(char codePoint, int modifiers) { - charConsumer.accept(codePoint, modifiers); - } - }; - } - - interface KeyCodeConsumer { - void accept(int keycode, int scancode, int modifiers); - } - - interface CharConsumer { - void accept(char codePoint, int modifiers); - } -} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java new file mode 100644 index 00000000..39780715 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java @@ -0,0 +1,161 @@ +package dev.isxander.controlify.screenkeyboard; + +import com.mojang.blaze3d.platform.InputConstants; +import dev.isxander.controlify.api.bind.InputBinding; +import dev.isxander.controlify.bindings.ControlifyBindings; +import dev.isxander.controlify.controller.ControllerEntity; +import dev.isxander.controlify.font.BindingFontHelper; +import dev.isxander.controlify.screenop.ComponentProcessor; +import dev.isxander.controlify.screenop.ScreenControllerEventListener; +import dev.isxander.controlify.screenop.ScreenProcessor; +import dev.isxander.controlify.utils.CUtil; +import dev.isxander.controlify.utils.HoldRepeatHelper; +import dev.isxander.controlify.utils.render.Blit; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.lwjgl.glfw.GLFW; + +public class KeyWidget extends AbstractWidget implements ComponentProcessor, ScreenControllerEventListener { + public static final ResourceLocation SPRITE = CUtil.rl("keyboard/key"); + + private final KeyboardWidget keyboard; + private final KeyboardLayout.ShiftableKey key; + + private final Component unshiftedLabel, shiftedLabel; + + private boolean shortcutPressed; + private final HoldRepeatHelper holdRepeatHelper; + + public KeyWidget(int x, int y, int width, int height, KeyboardLayout.ShiftableKey key, KeyboardWidget keyboard) { + super(x, y, width, height, Component.literal("Key")); + this.keyboard = keyboard; + this.key = key; + this.holdRepeatHelper = new HoldRepeatHelper(10, 2); + + this.unshiftedLabel = createLabel(key, false); + this.shiftedLabel = createLabel(key, true); + } + + public void renderKeyBackground(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + Blit.sprite(graphics, SPRITE, getX() + 1, getY() + 1, getWidth() - 2, getHeight() - 2); + + if (isHoveredOrFocused() || this.shortcutPressed) { + graphics.renderOutline(getX(), getY(), getWidth(), getHeight(), -1); + } else { + this.holdRepeatHelper.reset(); + } + } + + public void renderKeyForeground(GuiGraphics graphics, int mouseX, int mouseY, float deltaTick) { + Component label = this.keyboard.shifting ? this.shiftedLabel : this.unshiftedLabel; + graphics.drawCenteredString( + Minecraft.getInstance().font, + label, + getX() + getWidth() / 2, + getY() + getHeight() / 2 - 4, + 0xFFFFFFFF + ); + } + + @Override + protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + // custom rendered above + } + + @Override + public boolean overrideControllerButtons(ScreenProcessor screen, ControllerEntity controller) { + if (holdRepeatHelper.shouldAction(ControlifyBindings.GUI_PRESS.on(controller))) { + onPress(); + holdRepeatHelper.onNavigate(); + } + + return true; + } + + @Override + public void onControllerInput(ControllerEntity controller) { + this.key.shortcutBinding().ifPresent(supplier -> { + InputBinding shortcutBinding = supplier.on(controller); + + this.shortcutPressed = shortcutBinding.digitalNow(); + + if (this.holdRepeatHelper.shouldAction(shortcutBinding)) { + onPress(); + this.holdRepeatHelper.onNavigate(); + } + }); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button /*? if >=1.21.9 {*/, boolean doubleClick /*?}*/) { + if (isMouseOver(mouseX, mouseY)) { + onPress(); + return true; + } + return false; + } + + private void onPress() { + KeyboardLayout.Key key = this.getKey(); + KeyboardInputConsumer inputConsumer = this.keyboard.inputConsumer; + + switch (key) { + case KeyboardLayout.Key.StringKey stringKey -> { + stringKey.string().codePoints().forEach((codePoint) -> { + // guess the modifier based on the nature of the character + int modCapital = Character.isUpperCase(codePoint) ? GLFW.GLFW_MOD_SHIFT : 0; + int modifiers = modCapital; + + if (Character.isBmpCodePoint(codePoint)) { + inputConsumer.acceptChar((char) codePoint, modifiers); + } else if (Character.isValidCodePoint(codePoint)) { + inputConsumer.acceptChar(Character.highSurrogate(codePoint), modifiers); + inputConsumer.acceptChar(Character.lowSurrogate(codePoint), modifiers); + } + }); + } + + case KeyboardLayout.Key.CodeKey codeKey -> + inputConsumer.acceptKeyCode(codeKey.keycode(), codeKey.scancode(), codeKey.modifier()); + + case KeyboardLayout.Key.SpecialKey specialKey -> { + switch (specialKey.action()) { + case SHIFT -> this.keyboard.shifting = !this.keyboard.shifting; + + case ENTER -> inputConsumer.acceptKeyCode(InputConstants.KEY_RETURN, 0, 0); + case BACKSPACE -> inputConsumer.acceptKeyCode(InputConstants.KEY_BACKSPACE, 0, 0); + case TAB -> inputConsumer.acceptKeyCode(InputConstants.KEY_TAB, 0, 0); + case LEFT_ARROW -> inputConsumer.acceptKeyCode(InputConstants.KEY_LEFT, 0, 0); + case RIGHT_ARROW -> inputConsumer.acceptKeyCode(InputConstants.KEY_RIGHT, 0, 0); + case UP_ARROW -> inputConsumer.acceptKeyCode(InputConstants.KEY_UP, 0, 0); + case DOWN_ARROW -> inputConsumer.acceptKeyCode(InputConstants.KEY_DOWN, 0, 0); + } + } + } + } + + private KeyboardLayout.Key getKey() { + return this.key.get(this.keyboard.shifting); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + + } + + private static Component createLabel(KeyboardLayout.ShiftableKey shiftableKey, boolean shift) { + KeyboardLayout.Key key = shiftableKey.get(shift); + + return shiftableKey.shortcutBinding() + .map(b -> BindingFontHelper.binding(b.bindId())) + .map(glyph -> Component.empty() + .append(glyph) + .append(" ") + .append(key.displayName())) + .orElseGet(key::displayName); + } +} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardInputConsumer.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardInputConsumer.java new file mode 100644 index 00000000..44063b15 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardInputConsumer.java @@ -0,0 +1,7 @@ +package dev.isxander.controlify.screenkeyboard; + +public interface KeyboardInputConsumer { + void acceptChar(char ch, int modifiers); + + void acceptKeyCode(int keycode, int scancode, int modifiers); +} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java new file mode 100644 index 00000000..83ab666b --- /dev/null +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java @@ -0,0 +1,198 @@ +package dev.isxander.controlify.screenkeyboard; + +import com.mojang.datafixers.util.Either; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.isxander.controlify.api.bind.InputBindingSupplier; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.util.ExtraCodecs; +import net.minecraft.util.StringRepresentable; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; + +public record KeyboardLayout(float rowWidth, List> keys) { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.floatRange(1, Float.MAX_VALUE).fieldOf("width").forGetter(KeyboardLayout::rowWidth), + ShiftableKey.CODEC + .listOf(1, Integer.MAX_VALUE) + .listOf(1, Integer.MAX_VALUE) + .fieldOf("keys").forGetter(KeyboardLayout::keys) + ).apply(instance, KeyboardLayout::new)).validate( + layout -> + validateRowWidths(layout) + ? DataResult.success(layout) + : DataResult.error(() -> "Row widths do not match the specified row width: " + layout.rowWidth()) + ); + + public static boolean validateRowWidths(KeyboardLayout layout) { + return layout.keys().stream() + .mapToDouble(row -> row.stream() + .mapToDouble(ShiftableKey::width) + .sum()) + .allMatch(rowWidth -> rowWidth == layout.rowWidth()); + } + + public record ShiftableKey(Key regular, Key shifted, Optional shortcutBinding) { + private static final Codec PAIR_CODEC = Codec.withAlternative( + RecordCodecBuilder.create(instance -> instance.group( + Key.CODEC.fieldOf("regular").forGetter(ShiftableKey::regular), + Key.CODEC.optionalFieldOf("shifted").forGetter(k -> Optional.of(k.shifted)), + InputBindingSupplier.CODEC.optionalFieldOf("shortcut").forGetter(ShiftableKey::shortcutBinding) + ).apply(instance, ShiftableKey::new)), + Key.CODEC.listOf(2, 2).xmap( + list -> new ShiftableKey(list.get(0), list.get(1), Optional.empty()), + key -> List.of(key.regular(), key.shifted()) + ) + ); + + public static Codec CODEC = Codec.either(Key.CODEC, PAIR_CODEC) + .xmap( + either -> either.map(ShiftableKey::new, Function.identity()), + Either::right + ).validate(key -> key.validateWidths() + ? DataResult.success(key) + : DataResult.error(() -> "Regular and shifted keys have different widths: " + key) + ); + + public ShiftableKey(Key key) { + this(key, key.createUppercase().orElse(key), Optional.empty()); + } + + public ShiftableKey(Key regular, Optional shifted, Optional shortcutBinding) { + this(regular, shifted.orElse(regular.createUppercase().orElse(regular)), shortcutBinding); + } + + public Key get(boolean shifted) { + return shifted ? this.shifted : this.regular; + } + + public float width() { + return regular.width(); + } + + public boolean validateWidths() { + return regular().width() == shifted().width(); + } + } + + public sealed interface Key { + float width(); + + Component displayName(); + + default Optional createUppercase() { + return Optional.empty(); + } + + Codec CODEC = new Codec<>() { + @Override + public DataResult encode(Key input, DynamicOps ops, T prefix) { + return switch (input) { + case StringKey stringKey -> StringKey.CODEC.encode(stringKey, ops, prefix); + case CodeKey codeKey -> CodeKey.CODEC.encode(codeKey, ops, prefix); + case SpecialKey specialKey -> SpecialKey.CODEC.encode(specialKey, ops, prefix); + }; + } + + @Override + public DataResult> decode(DynamicOps ops, T input) { + return Stream.of(StringKey.CODEC, CodeKey.CODEC, SpecialKey.CODEC) + .map(decoder -> decoder.decode(ops, input)) + .map(r -> r.map(p -> p.mapFirst(t -> (Key) t))) + .filter(DataResult::isSuccess) + .findFirst() + .orElseGet(() -> DataResult.error(() -> "No decoder matched.")); + } + }; + + record StringKey(String string, float width) implements Key { + private static final Codec STR_CODEC = Codec.string(1, Integer.MAX_VALUE); + + public static final Codec CODEC = Codec.withAlternative( + RecordCodecBuilder.create(instance -> instance.group( + STR_CODEC.fieldOf("chars").forGetter(StringKey::string), + Codec.FLOAT.optionalFieldOf("width", 1.0f).forGetter(StringKey::width) + ).apply(instance, StringKey::new)), + STR_CODEC.xmap(StringKey::new, StringKey::string) + ); + + public StringKey(String string) { + this(string, 1.0f); + } + + @Override + public Component displayName() { + return Component.literal(string); + } + + @Override + public Optional createUppercase() { + return Optional.of(new StringKey(string.toUpperCase(), width)); + } + } + + record CodeKey(int keycode, int scancode, int modifier, Component displayName, float width) implements Key { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.INT.fieldOf("keycode").forGetter(CodeKey::keycode), + Codec.INT.optionalFieldOf("scancode", 0).forGetter(CodeKey::scancode), + Codec.INT.optionalFieldOf("modifier", 0).forGetter(CodeKey::modifier), + ComponentSerialization.CODEC.fieldOf("display_name").forGetter(CodeKey::displayName), + Codec.FLOAT.optionalFieldOf("width", 1.0f).forGetter(CodeKey::width) + ).apply(instance, CodeKey::new)); + + public CodeKey(int keycode, int scancode, int modifier, Component displayName) { + this(keycode, scancode, modifier, displayName, 1.0f); + } + } + + record SpecialKey(Action action, float width) implements Key { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + StringRepresentable.fromEnum(Action::values).fieldOf("action").forGetter(SpecialKey::action), + Codec.FLOAT.optionalFieldOf("width", 1.0f).forGetter(SpecialKey::width) + ).apply(instance, SpecialKey::new)); + + public SpecialKey(Action action) { + this(action, 1.0f); + } + + @Override + public Component displayName() { + return action().displayName(); + } + + public enum Action implements StringRepresentable { + SHIFT("shift"), + ENTER("enter"), + BACKSPACE("backspace"), + TAB("tab"), + LEFT_ARROW("left_arrow"), + RIGHT_ARROW("right_arrow"), + UP_ARROW("up_arrow"), + DOWN_ARROW("down_arrow"); + + private final String serialName; + + Action(String serialName) { + this.serialName = serialName; + } + + @Override + public @NotNull String getSerializedName() { + return this.serialName; + } + + public Component displayName() { + return Component.translatable("controlify.keyboard.special." + this.serialName); + } + } + } + } +} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutManager.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutManager.java new file mode 100644 index 00000000..d49b2f86 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutManager.java @@ -0,0 +1,89 @@ +package dev.isxander.controlify.screenkeyboard; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; +import dev.isxander.controlify.platform.client.resource.SimpleControlifyReloadListener; +import dev.isxander.controlify.utils.CUtil; +import dev.isxander.controlify.utils.log.ControlifyLogger; +import net.minecraft.Util; +import net.minecraft.resources.FileToIdConverter; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; + +import java.io.BufferedReader; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; + +public class KeyboardLayoutManager implements SimpleControlifyReloadListener { + + private static final String PREFIX = "keyboard_layout"; + private static final FileToIdConverter fileToIdConverter = FileToIdConverter.json(PREFIX); + private static final ControlifyLogger LOGGER = CUtil.LOGGER.createSubLogger("KeyboardLayoutManager"); + + private Map layouts = Map.of(); + + @Override + public CompletableFuture load(ResourceManager manager, Executor executor) { + return CompletableFuture.supplyAsync( + () -> fileToIdConverter.listMatchingResources(manager), + executor + ).thenCompose(layoutMap -> { + var futures = layoutMap.entrySet().stream() + .map(entry -> loadLayout(entry.getKey(), entry.getValue(), executor)) + .toList(); + + var map = Util.sequence(futures) + .thenApply(listOfEntries -> listOfEntries.stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a))); + + return map.thenApply(Preparations::new); + }); + } + + private CompletableFuture> loadLayout( + ResourceLocation file, Resource resource, Executor executor + ) { + return CompletableFuture.supplyAsync(() -> { + ResourceLocation id = fileToIdConverter.fileToId(file); + + try (BufferedReader reader = resource.openAsReader()) { + JsonElement json = JsonParser.parseReader(reader); + KeyboardLayout layout = KeyboardLayout.CODEC.parse(JsonOps.INSTANCE, json) + .getOrThrow(reason -> new RuntimeException("Failed to parse keyboard layout " + id + ": " + reason)); + + return Map.entry(id, layout); + } catch (Exception e) { + throw new RuntimeException("Failed to read keyboard layout " + id + ": " + e.getMessage(), e); + } + }, executor); + } + + @Override + public CompletableFuture apply(Preparations data, ResourceManager manager, Executor executor) { + return CompletableFuture.runAsync(() -> { + this.layouts = data.layouts(); + LOGGER.log("Loaded {} keyboard layouts", layouts.size()); + }, executor); + } + + public Map getLayouts() { + return Collections.unmodifiableMap(layouts); + } + + public KeyboardLayout getLayout(ResourceLocation id) { + return Objects.requireNonNull(layouts.get(id)); + } + + @Override + public ResourceLocation getReloadId() { + return CUtil.rl("keyboard_layout"); + } + + public record Preparations(Map layouts) {} +} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayouts.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayouts.java new file mode 100644 index 00000000..8c028353 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayouts.java @@ -0,0 +1,16 @@ +package dev.isxander.controlify.screenkeyboard; + +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.utils.CUtil; +import net.minecraft.resources.ResourceLocation; + +public final class KeyboardLayouts { + + public static final ResourceLocation CHAT = CUtil.rl("chat"); + + public static KeyboardLayout chat() { + return Controlify.instance().keyboardLayoutManager().getLayout(CHAT); + } + + private KeyboardLayouts() {} +} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java index a959a506..a505baa2 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java @@ -1,19 +1,7 @@ package dev.isxander.controlify.screenkeyboard; -import com.mojang.datafixers.util.Pair; -import dev.isxander.controlify.api.ControlifyApi; -import dev.isxander.controlify.api.bind.InputBinding; -import dev.isxander.controlify.api.bind.InputBindingSupplier; -import dev.isxander.controlify.bindings.ControlifyBindings; -import dev.isxander.controlify.controller.ControllerEntity; -import dev.isxander.controlify.screenop.ComponentProcessor; -import dev.isxander.controlify.screenop.ScreenControllerEventListener; -import dev.isxander.controlify.screenop.ScreenProcessor; import dev.isxander.controlify.screenop.ScreenProcessorProvider; -import dev.isxander.controlify.utils.render.*; -import dev.isxander.controlify.utils.CUtil; -import dev.isxander.controlify.utils.HoldRepeatHelper; -import net.minecraft.client.Minecraft; +import dev.isxander.controlify.utils.render.Blit; import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; @@ -23,40 +11,68 @@ import net.minecraft.client.gui.navigation.FocusNavigationEvent; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; -import org.apache.commons.lang3.Validate; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.lwjgl.glfw.GLFW; -import java.util.*; -import java.util.function.BiConsumer; -import java.util.function.Consumer; +import java.util.ArrayList; +import java.util.List; -public abstract class KeyboardWidget extends AbstractWidget implements ContainerEventHandler { - protected final List keys; - protected final KeyPressConsumer keyPressConsumer; +public class KeyboardWidget extends AbstractWidget implements ContainerEventHandler { + private final KeyboardLayout layout; + final KeyboardInputConsumer inputConsumer; - protected boolean shiftMode; + private final List keys; + + boolean shifting; private @Nullable GuiEventListener focused; private boolean isDragging; private final Screen containingScreen; - public KeyboardWidget(Screen screen, int x, int y, int width, int height, KeyPressConsumer keyPressConsumer) { - super(x, y, width, height, Component.literal("On-screen keyboard")); - this.containingScreen = screen; - this.keyPressConsumer = keyPressConsumer; - this.keys = new ArrayList<>(); - arrangeKeys(); + public KeyboardWidget(int x, int y, int width, int height, KeyboardLayout layout, KeyboardInputConsumer inputConsumer, Screen containingScreen) { + super(x, y, width, height, Component.literal("On-Screen Keyboard")); + this.layout = layout; + this.inputConsumer = inputConsumer; + this.containingScreen = containingScreen; + + int keyCount = this.layout.keys().stream() + .mapToInt(List::size) + .sum(); + this.keys = new ArrayList<>(keyCount); + this.arrangeKeys(); + System.out.println(layout); } - protected abstract void arrangeKeys(); + private void arrangeKeys() { + this.keys.clear(); + + float unitWidth = (float) this.getWidth() / this.layout.rowWidth(); + int keyHeight = (int)((float) this.getHeight() / this.layout.keys().size()); + + int y = this.getY(); + for (List row : this.layout.keys()) { + int x = this.getX(); + for (KeyboardLayout.ShiftableKey key : row) { + int keyWidth = (int) (key.regular().width() * unitWidth); + + var keyWidget = new KeyWidget( + x, y, keyWidth, keyHeight, + key, this + ); + ScreenProcessorProvider.provide(this.containingScreen).addEventListener(keyWidget); + + this.keys.add(keyWidget); + + x += keyWidth; + } + y += keyHeight; + } + } @Override protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { - for (T key : keys) { + for (KeyWidget key : keys) { // vanilla widget render does other stuff like mouse hover update etc // render method of keys are empty - this doesn't actually do any rendering key.render(guiGraphics, mouseX, mouseY, partialTick); @@ -68,13 +84,13 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo guiGraphics.fill(getX(), getY(), getX() + getWidth(), getY() + getHeight(), 0x80000000); guiGraphics.renderOutline(getX(), getY(), getWidth(), getHeight(), 0xFFAAAAAA); - for (T key : keys) { + for (KeyWidget key : keys) { // every key background is rendered into the same vertex buffer to upload at once key.renderKeyBackground(guiGraphics, mouseX, mouseY, partialTick); } // renders all foreground after background to prevent context switching - for (T key : keys) { + for (KeyWidget key : keys) { // text rendering is batched by default in managed mode key.renderKeyForeground(guiGraphics, mouseX, mouseY, partialTick); } @@ -82,261 +98,13 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo } @Override - protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { - - } - - public static class Key extends AbstractWidget implements ComponentProcessor, ScreenControllerEventListener { - public static final ResourceLocation SPRITE = CUtil.rl("keyboard/key"); - - private final KeyboardWidget keyboard; - - private final KeyFunction normalFunction; - private final KeyFunction shiftedFunction; - - private boolean highlighted; - - private final HoldRepeatHelper holdRepeatHelper; - - private final InputBindingSupplier shortcutPressBind; - private boolean shortcutPressed; - - public Key(Screen screen, int x, int y, int width, int height, KeyFunction normalFunction, @Nullable KeyFunction shiftedFunction, KeyboardWidget keyboard, @Nullable InputBindingSupplier shortcutPressBind) { - super(x, y, width, height, Component.literal("Key")); - this.keyboard = keyboard; - this.normalFunction = normalFunction; - if (shiftedFunction != null) - this.shiftedFunction = shiftedFunction; - else - this.shiftedFunction = normalFunction; - this.holdRepeatHelper = new HoldRepeatHelper(10, 2); - this.shortcutPressBind = shortcutPressBind; - ScreenProcessorProvider.provide(screen).addEventListener(this); - } - - public Key(Screen screen, int x, int y, int width, int height, Pair functions, KeyboardWidget keyboard, @Nullable InputBindingSupplier shortcutPressBind) { - this(screen, x, y, width, height, functions.getFirst(), functions.getSecond(), keyboard, shortcutPressBind); - } - - protected void renderKeyBackground(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { - Blit.sprite(graphics, SPRITE, getX() + 1, getY() + 1, getWidth() - 2, getHeight() - 2); - } - - protected void renderKeyForeground(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { - if (keyboard.shiftMode) { - shiftedFunction.renderer.render(graphics, mouseX, mouseY, partialTick, this); - } else { - normalFunction.renderer.render(graphics, mouseX, mouseY, partialTick, this); - } - - if (isHoveredOrFocused() || shortcutPressed) { - graphics.renderOutline(getX(), getY(), getWidth(), getHeight(), -1); - } else { - holdRepeatHelper.reset(); - } - } - - @Override - protected void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { - // custom rendered above - } - - @Override - public boolean overrideControllerButtons(ScreenProcessor screen, ControllerEntity controller) { - if (holdRepeatHelper.shouldAction(ControlifyBindings.GUI_PRESS.on(controller))) { - onPress(); - holdRepeatHelper.onNavigate(); - } - - // do not allow any other input on keys to be processed - return true; - } - - @Override - public void onControllerInput(ControllerEntity controller) { - if (shortcutPressBind == null) return; - - InputBinding shortcutBind = shortcutPressBind.on(controller); - - shortcutPressed = shortcutBind.digitalNow(); - - if (holdRepeatHelper.shouldAction(shortcutBind)) { - onPress(); - holdRepeatHelper.onNavigate(); - } - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button /*? if >=1.21.9 {*/ ,boolean doubleClick /*?}*/) { - if (isMouseOver(mouseX, mouseY)) { - onPress(); - return true; - } - return false; - } - - protected void onPress() { - if (keyboard.shiftMode) { - shiftedFunction.consumer.accept(keyboard.keyPressConsumer, this); - } else { - normalFunction.consumer.accept(keyboard.keyPressConsumer, this); - } - } - - public Component modifyKeyName(Component name) { - Optional controller = ControlifyApi.get().getCurrentController() - .filter(c -> ControlifyApi.get().currentInputMode().isController()); - if (shortcutPressBind != null && controller.isPresent()) { - InputBinding binding = shortcutPressBind.on(controller.get()); - - return Component.empty() - .append(binding.inputGlyph()) - .append(name); - } - - return name; - } - - @Override - protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { - - } - - public void setHighlighted(boolean highlighted) { - this.highlighted = highlighted; - } - - public boolean isHighlighted() { - return highlighted; - } - - public static KeyBuilder builder(Pair functions, @Nullable InputBindingSupplier shortcutPressBind) { - return (screen, x, y, w, h, kb) -> new Key(screen, x, y, w, h, functions.getFirst(), functions.getSecond(), kb, shortcutPressBind); - } - - public static KeyBuilder builder(KeyFunction normalFunction, KeyFunction shiftedFunction, @Nullable InputBindingSupplier shortcutPressBind) { - return (screen, x, y, w, h, kb) -> new Key(screen, x, y, w, h, normalFunction, shiftedFunction, kb, shortcutPressBind); - } - - public interface ForegroundRenderer { - void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick, Key key); - - static ForegroundRenderer text(Component text) { - return (guiGraphics, mouseX, mouseY, partialTick, key) -> { - guiGraphics.drawCenteredString(Minecraft.getInstance().font, key.modifyKeyName(text), key.getX() + key.getWidth() / 2, key.getY() + key.getHeight() / 2 - 4, 0xFFFFFFFF); - }; - } - } - } - - public record KeyFunction(BiConsumer consumer, Key.ForegroundRenderer renderer) { - public static Pair ofChar(int normalKeyCode, char normalChar, int normalModifier, int shiftedKeyCode, char shiftedChar, int shiftedModifier) { - return Pair.of( - new KeyFunction((screen, key) -> { - screen.acceptKeyCode(normalKeyCode, 0, normalModifier); - screen.acceptChar(normalChar, normalModifier); - }, Key.ForegroundRenderer.text(Component.literal(String.valueOf(normalChar)))), - new KeyFunction((screen, key) -> { - screen.acceptKeyCode(shiftedKeyCode, 0, shiftedModifier); - screen.acceptChar(shiftedChar, shiftedModifier); - }, Key.ForegroundRenderer.text(Component.literal(String.valueOf(shiftedChar))) - ) - ); - } - - public static Pair ofChar(int keyCode, char ch) { - return ofChar(keyCode, Character.toLowerCase(ch), 0, keyCode, Character.toUpperCase(ch), GLFW.GLFW_MOD_SHIFT); - } - - public static Pair ofRegularKey(int keycode, String normal) { - KeyFunction function = new KeyFunction((screen, key) -> screen.acceptKeyCode(keycode, 0, 0), Key.ForegroundRenderer.text(Component.literal(normal))); - return Pair.of(function, function); - } - - public static Pair ofShiftableKey(int normalKeyCode, int normalModifier, String normalName, int shiftKeyCode, int shiftModifier, String shiftName) { - return Pair.of( - new KeyFunction((screen, key) -> screen.acceptKeyCode(normalKeyCode, 0, normalModifier), Key.ForegroundRenderer.text(Component.literal(normalName))), - new KeyFunction((screen, key) -> screen.acceptKeyCode(shiftKeyCode, 0, shiftModifier), Key.ForegroundRenderer.text(Component.literal(shiftName))) - ); - } - - public static Pair ofShiftableKey(int keyCode, String normal, String shift) { - return ofShiftableKey(keyCode, 0, normal, keyCode, GLFW.GLFW_MOD_SHIFT, shift); - } - - public Pair copyShifted() { - return Pair.of(this, this); - } - } - - public static class KeyLayoutBuilder { - private final List>> layout; - private final float maxUnitWidth; - private final int rowCount; - - private final KeyboardWidget keyboard; - - private float currentWidth; - private int currentRow; - - public KeyLayoutBuilder(float maxUnitWidth, int rowCount, KeyboardWidget keyboard) { - this.maxUnitWidth = maxUnitWidth; - this.rowCount = rowCount; - this.keyboard = keyboard; - this.layout = new ArrayList<>(rowCount); - for (int i = 0; i < rowCount; i++) { - layout.add(new ArrayList<>()); - } - } - - public void key(KeyBuilder key, float width) { - Validate.isTrue(currentWidth + width <= maxUnitWidth, "Key width exceeds row width"); - - layout.get(currentRow).add(new KeyLayout<>(key, width)); - - currentWidth += width; - } - - public void nextRow() { - Validate.isTrue(currentRow < rowCount, "Row index out of bounds"); - - currentWidth = 0; - currentRow++; - } - - public void build(Consumer keyConsumer) { - int trueWidth = keyboard.getWidth(); - int trueHeight = keyboard.getHeight(); - - float unitWidth = (float) trueWidth / maxUnitWidth; - float keyHeight = (float) trueHeight / rowCount; - - float y = keyboard.getY(); - for (List> row : layout) { - float x = keyboard.getX(); - for (KeyLayout keyLayout : row) { - float keyWidth = unitWidth * keyLayout.unitWidth; - T key = keyLayout.keyBuilder.build(keyboard.containingScreen, (int) x, (int) y, (int) keyWidth, (int) keyHeight, keyboard); - keyConsumer.accept(key); - - x += keyWidth; - } - - y += keyHeight; - } - } - - private record KeyLayout(KeyBuilder keyBuilder, float unitWidth) {} - } - - @FunctionalInterface - public interface KeyBuilder { - T build(Screen screen, int x, int y, int width, int height, KeyboardWidget keyboard); + public @NotNull List children() { + return this.keys; } @Override - public @NotNull List children() { - return keys; + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + } @Override @@ -349,9 +117,8 @@ public void setDragging(boolean dragging) { isDragging = dragging; } - @Nullable @Override - public GuiEventListener getFocused() { + public @Nullable GuiEventListener getFocused() { return focused; } @@ -398,4 +165,5 @@ public boolean isFocused() { public void setFocused(boolean focused) { ContainerEventHandler.super.setFocused(focused); } + } diff --git a/src/main/java/dev/isxander/controlify/utils/FuzzyMapCodec.java b/src/main/java/dev/isxander/controlify/utils/codec/FuzzyMapCodec.java similarity index 96% rename from src/main/java/dev/isxander/controlify/utils/FuzzyMapCodec.java rename to src/main/java/dev/isxander/controlify/utils/codec/FuzzyMapCodec.java index 02f2809a..14da7293 100644 --- a/src/main/java/dev/isxander/controlify/utils/FuzzyMapCodec.java +++ b/src/main/java/dev/isxander/controlify/utils/codec/FuzzyMapCodec.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.utils; +package dev.isxander.controlify.utils.codec; import com.mojang.serialization.*; diff --git a/src/main/java/dev/isxander/controlify/utils/SetCodec.java b/src/main/java/dev/isxander/controlify/utils/codec/SetCodec.java similarity index 98% rename from src/main/java/dev/isxander/controlify/utils/SetCodec.java rename to src/main/java/dev/isxander/controlify/utils/codec/SetCodec.java index a7c30a33..ce3db7cb 100644 --- a/src/main/java/dev/isxander/controlify/utils/SetCodec.java +++ b/src/main/java/dev/isxander/controlify/utils/codec/SetCodec.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.utils; +package dev.isxander.controlify.utils.codec; import com.mojang.datafixers.util.Pair; import com.mojang.datafixers.util.Unit; diff --git a/src/main/java/dev/isxander/controlify/utils/StrictEitherMapCodec.java b/src/main/java/dev/isxander/controlify/utils/codec/StrictEitherMapCodec.java similarity index 96% rename from src/main/java/dev/isxander/controlify/utils/StrictEitherMapCodec.java rename to src/main/java/dev/isxander/controlify/utils/codec/StrictEitherMapCodec.java index 292cb544..5dabe3fc 100644 --- a/src/main/java/dev/isxander/controlify/utils/StrictEitherMapCodec.java +++ b/src/main/java/dev/isxander/controlify/utils/codec/StrictEitherMapCodec.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.utils; +package dev.isxander.controlify.utils.codec; import com.mojang.serialization.*; diff --git a/src/main/resources/assets/controlify/keyboard_layout/chat.json b/src/main/resources/assets/controlify/keyboard_layout/chat.json new file mode 100644 index 00000000..b76b7242 --- /dev/null +++ b/src/main/resources/assets/controlify/keyboard_layout/chat.json @@ -0,0 +1,8 @@ +{ + "width": 10, + "keys": [ + [ ["q", "qwerty!"], "w", "e", "r", "t", "y", "u", "i", "o", "p" ], + [ {"regular": {"action": "shift"}, "shortcut": "controlify:gui_abstract_action_3"}, "a", "s", "d", "f", "g", "h", "j", "k", "l" ], + [ " ", " ", "z", "x", "c", "v", "b", "n", "m", " " ] + ] +} diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json index c25eb22a..1080e5eb 100644 --- a/src/main/resources/controlify.mixins.json +++ b/src/main/resources/controlify.mixins.json @@ -63,6 +63,7 @@ "feature.rumble.useitem.LocalPlayerMixin", "feature.rumble.waterland.LocalPlayerMixin", "feature.rumble.waterland.PlayerMixin", + "feature.screenkeyboard.AbstractSignEditScreenMixin", "feature.screenkeyboard.ChatComponentMixin", "feature.screenkeyboard.ChatScreenMixin", "feature.screenkeyboard.CommandSuggestionsMixin", From 22c5751ca3f7b796f219c57985a02fc3b55891de Mon Sep 17 00:00:00 2001 From: isxander Date: Wed, 6 Aug 2025 21:42:46 +0100 Subject: [PATCH 02/12] localisation, key press sprite, fallback code-based layout, more touching up --- .../isxander/controlify/api/guide/Rule.java | 5 +- .../controlify/bindings/input/InputType.java | 3 +- .../controlify/config/GlobalSettings.java | 3 +- .../controller/id/ControllerTypeManager.java | 2 +- .../gui/guide/InGameButtonGuide.java | 3 +- .../screen/GlobalSettingsScreenFactory.java | 3 +- .../controlify/hid/HIDIdentifier.java | 18 +-- .../feature/font/KeybindContentsMixin.java | 10 +- .../screenkeyboard/ChatScreenMixin.java | 6 + .../FallbackKeyboardLayout.java | 80 ++++++++++ .../controlify/screenkeyboard/KeyWidget.java | 123 +++++++++++---- .../screenkeyboard/KeyboardLayout.java | 140 ++++++++++++------ .../screenkeyboard/KeyboardLayoutManager.java | 71 +++++++-- .../screenkeyboard/KeyboardLayoutWithId.java | 6 + .../screenkeyboard/KeyboardLayouts.java | 6 +- .../screenkeyboard/KeyboardWidget.java | 114 ++++++++++---- .../controlify/screenop/ScreenProcessor.java | 9 +- .../controlify/utils/codec/CExtraCodecs.java | 59 ++++++++ .../controlify/utils/codec/SetCodec.java | 4 - .../controlify/keyboard_layout/chat.json | 8 - .../keyboard_layout/chat/en_us.json | 80 ++++++++++ .../assets/controlify/lang/en_us.json | 13 ++ .../gui/sprites/keyboard/key_pressed.png | Bin 0 -> 162 bytes .../sprites/keyboard/key_pressed.png.mcmeta | 15 ++ 24 files changed, 630 insertions(+), 151 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/FallbackKeyboardLayout.java create mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutWithId.java create mode 100644 src/main/java/dev/isxander/controlify/utils/codec/CExtraCodecs.java delete mode 100644 src/main/resources/assets/controlify/keyboard_layout/chat.json create mode 100644 src/main/resources/assets/controlify/keyboard_layout/chat/en_us.json create mode 100644 src/main/resources/assets/controlify/textures/gui/sprites/keyboard/key_pressed.png create mode 100644 src/main/resources/assets/controlify/textures/gui/sprites/keyboard/key_pressed.png.mcmeta diff --git a/src/main/java/dev/isxander/controlify/api/guide/Rule.java b/src/main/java/dev/isxander/controlify/api/guide/Rule.java index 24ecb9c1..3d689e3f 100644 --- a/src/main/java/dev/isxander/controlify/api/guide/Rule.java +++ b/src/main/java/dev/isxander/controlify/api/guide/Rule.java @@ -3,6 +3,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import dev.isxander.controlify.api.bind.InputBindingSupplier; +import dev.isxander.controlify.utils.codec.CExtraCodecs; import dev.isxander.controlify.utils.codec.SetCodec; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentSerialization; @@ -35,8 +36,8 @@ public record Rule( instance -> instance.group( InputBindingSupplier.CODEC.fieldOf("for").forGetter(Rule::binding), ActionLocation.CODEC.fieldOf("where").forGetter(Rule::where), - new SetCodec<>(ResourceLocation.CODEC).optionalFieldOf("when", Set.of()).forGetter(Rule::when), - new SetCodec<>(ResourceLocation.CODEC).optionalFieldOf("forbid", Set.of()).forGetter(Rule::forbid), + CExtraCodecs.set(ResourceLocation.CODEC).optionalFieldOf("when", Set.of()).forGetter(Rule::when), + CExtraCodecs.set(ResourceLocation.CODEC).optionalFieldOf("forbid", Set.of()).forGetter(Rule::forbid), ComponentSerialization.CODEC.fieldOf("then").forGetter(Rule::then) ).apply(instance, Rule::new) ); diff --git a/src/main/java/dev/isxander/controlify/bindings/input/InputType.java b/src/main/java/dev/isxander/controlify/bindings/input/InputType.java index 5c1fc384..a8affa50 100644 --- a/src/main/java/dev/isxander/controlify/bindings/input/InputType.java +++ b/src/main/java/dev/isxander/controlify/bindings/input/InputType.java @@ -3,6 +3,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import dev.isxander.controlify.utils.CUtil; +import dev.isxander.controlify.utils.codec.CExtraCodecs; import dev.isxander.controlify.utils.codec.FuzzyMapCodec; import dev.isxander.controlify.utils.codec.StrictEitherMapCodec; import net.minecraft.Util; @@ -27,7 +28,7 @@ public record InputType(String id, MapCodec codec) implement public static MapCodec createCodec( T[] types, Function> codecGetter, Function typeGetter, String typeFieldName ) { - MapCodec fuzzyCodec = new FuzzyMapCodec<>( + MapCodec fuzzyCodec = CExtraCodecs.fuzzyMap( Stream.of(types).map(codecGetter).toList(), obj -> codecGetter.apply(typeGetter.apply(obj)) ); diff --git a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java index a4879f15..54b04f66 100644 --- a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java +++ b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java @@ -2,7 +2,6 @@ import com.google.common.collect.Lists; import com.google.gson.annotations.SerializedName; -import dev.isxander.controlify.driver.steamdeck.SteamDeckUtil; import dev.isxander.controlify.reacharound.ReachAroundMode; import dev.isxander.controlify.server.ServerPolicies; import net.minecraft.client.Minecraft; @@ -27,7 +26,7 @@ public class GlobalSettings { public boolean vibrationOnboarded = false; public ReachAroundMode reachAround = ReachAroundMode.OFF; public boolean allowServerRumble = true; - public boolean uiSounds = false; + public boolean extraUiSounds = true; public boolean notifyLowBattery = true; public float ingameButtonGuideScale = 1f; public boolean useEnhancedSteamDeckDriver = true; diff --git a/src/main/java/dev/isxander/controlify/controller/id/ControllerTypeManager.java b/src/main/java/dev/isxander/controlify/controller/id/ControllerTypeManager.java index d1c82e3c..81e8634d 100644 --- a/src/main/java/dev/isxander/controlify/controller/id/ControllerTypeManager.java +++ b/src/main/java/dev/isxander/controlify/controller/id/ControllerTypeManager.java @@ -33,7 +33,7 @@ public class ControllerTypeManager implements SimpleControlifyReloadListener ENTRY_CODEC = RecordCodecBuilder.create(instance -> instance.group( - Codec.list(HIDIdentifier.LIST_CODEC) + Codec.list(HIDIdentifier.CODEC) .comapFlatMap(list -> list.isEmpty() ? DataResult.error(() -> "At least one HID must be present") : DataResult.success(list), list -> list) .fieldOf("hids") .forGetter(ControllerTypeEntry::hid), diff --git a/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java b/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java index 92e3bac9..7fd941a6 100644 --- a/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java +++ b/src/main/java/dev/isxander/controlify/gui/guide/InGameButtonGuide.java @@ -18,9 +18,10 @@ public InGameButtonGuide(ControllerEntity controller, Minecraft minecraft) { public void renderHud(GuiGraphics graphics, float tickDelta) { boolean debugOpen = minecraft.getDebugOverlay().showDebugScreen(); boolean hideGui = minecraft.options.hideGui; + boolean screenOpen = minecraft.screen != null; GenericControllerConfig config = controller.genericConfig().config(); - if (!debugOpen && !hideGui && config.showIngameGuide) { + if (!debugOpen && !hideGui && !screenOpen && config.showIngameGuide) { GuideRenderer.render(graphics, GuideDomains.IN_GAME, minecraft, config.ingameGuideBottom, true); } } diff --git a/src/main/java/dev/isxander/controlify/gui/screen/GlobalSettingsScreenFactory.java b/src/main/java/dev/isxander/controlify/gui/screen/GlobalSettingsScreenFactory.java index 7fee33f9..2e28a5ca 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/GlobalSettingsScreenFactory.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/GlobalSettingsScreenFactory.java @@ -5,7 +5,6 @@ import dev.isxander.controlify.config.GlobalSettings; import dev.isxander.controlify.controller.ControllerEntity; import dev.isxander.controlify.driver.steamdeck.SteamDeckUtil; -import dev.isxander.controlify.gui.controllers.FormattableStringController; import dev.isxander.controlify.reacharound.ReachAroundMode; import dev.isxander.controlify.server.ServerPolicies; import dev.isxander.controlify.server.ServerPolicy; @@ -129,7 +128,7 @@ public static Screen createGlobalSettingsScreen(Screen parent) { .description(OptionDescription.createBuilder() .text(Component.translatable("controlify.gui.ui_sounds.tooltip")) .build()) - .binding(GlobalSettings.DEFAULT.uiSounds, () -> globalSettings.uiSounds, v -> globalSettings.uiSounds = v) + .binding(GlobalSettings.DEFAULT.extraUiSounds, () -> globalSettings.extraUiSounds, v -> globalSettings.extraUiSounds = v) .controller(TickBoxControllerBuilder::create) .build()) .option(Option.createBuilder() diff --git a/src/main/java/dev/isxander/controlify/hid/HIDIdentifier.java b/src/main/java/dev/isxander/controlify/hid/HIDIdentifier.java index bc3ec5f8..6f50d16f 100644 --- a/src/main/java/dev/isxander/controlify/hid/HIDIdentifier.java +++ b/src/main/java/dev/isxander/controlify/hid/HIDIdentifier.java @@ -1,24 +1,20 @@ package dev.isxander.controlify.hid; import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; +import dev.isxander.controlify.utils.codec.CExtraCodecs; +import org.jetbrains.annotations.NotNull; import java.util.HexFormat; -import java.util.List; public record HIDIdentifier(int vendorId, int productId) { - public static final Codec LIST_CODEC = Codec.list(Codec.INT).comapFlatMap( - parts -> { - if (parts.size() != 2) { - return DataResult.error(() -> "HID identifier list must have exactly two elements, found " + parts.size()); - } - return DataResult.success(new HIDIdentifier(parts.get(0), parts.get(1))); - }, - hid -> List.of(hid.vendorId(), hid.productId()) + public static final Codec CODEC = CExtraCodecs.arrayPair( + Codec.INT, + HIDIdentifier::vendorId, HIDIdentifier::productId, + HIDIdentifier::new ); @Override - public String toString() { + public @NotNull String toString() { var hex = HexFormat.of(); return "HID[" + "VID=0x" + hex.toHexDigits(vendorId, 4) + diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/font/KeybindContentsMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/font/KeybindContentsMixin.java index 3c678325..c8e28834 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/font/KeybindContentsMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/font/KeybindContentsMixin.java @@ -18,6 +18,9 @@ import java.util.Optional; +//? if >=1.21.9 +import net.minecraft.network.chat.FontDescription; + @Mixin(KeybindContents.class) public class KeybindContentsMixin { @Shadow @@ -26,7 +29,12 @@ public class KeybindContentsMixin { @WrapOperation(method = "visit(Lnet/minecraft/network/chat/FormattedText$StyledContentConsumer;Lnet/minecraft/network/chat/Style;)Ljava/util/Optional;", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/chat/contents/KeybindContents;getNestedComponent()Lnet/minecraft/network/chat/Component;")) private Component testVisitWithStyle(KeybindContents instance, Operation original, @Local(argsOnly = true) Style style) { - boolean wrapperFont = BindingFontHelper.WRAPPER_FONT.equals(style.getFont()); + //? if >=1.21.9 { + boolean wrapperFont = style.getFont() instanceof FontDescription.Resource(ResourceLocation font) + && BindingFontHelper.WRAPPER_FONT.equals(font); + //?} else { + /*boolean wrapperFont = BindingFontHelper.WRAPPER_FONT.equals(style.getFont()); + *///?} if (wrapperFont) { Optional inputText = ControlifyApi.get().getCurrentController() diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java index fc70ae01..0ac5d673 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java @@ -8,6 +8,7 @@ import dev.isxander.controlify.screenkeyboard.KeyboardLayouts; import dev.isxander.controlify.screenkeyboard.KeyboardWidget; import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.screens.ChatScreen; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; @@ -68,6 +69,11 @@ public void acceptKeyCode(int keycode, int scancode, int modifiers) { }); } + @ModifyArg(method = "setInitialFocus", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/ChatScreen;setInitialFocus(Lnet/minecraft/client/gui/components/events/GuiEventListener;)V")) + private GuiEventListener modifyInitialFocus(GuiEventListener editBox) { + return this.keyboard != null ? this.keyboard : editBox; + } + @ModifyArg(method = "init", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/ChatScreen$1;(Lnet/minecraft/client/gui/screens/ChatScreen;Lnet/minecraft/client/gui/Font;IIIILnet/minecraft/network/chat/Component;)V"), index = 3) private int modifyInputBoxY(int y) { return (int) (y - this.height * this.shiftChatAmt); diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/FallbackKeyboardLayout.java b/src/main/java/dev/isxander/controlify/screenkeyboard/FallbackKeyboardLayout.java new file mode 100644 index 00000000..6486d4d2 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/FallbackKeyboardLayout.java @@ -0,0 +1,80 @@ +package dev.isxander.controlify.screenkeyboard; + +import dev.isxander.controlify.api.bind.InputBindingSupplier; +import dev.isxander.controlify.bindings.ControlifyBindings; +import dev.isxander.controlify.utils.CUtil; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Optional; + +import static dev.isxander.controlify.screenkeyboard.KeyboardLayout.ShiftableKey; +import static dev.isxander.controlify.screenkeyboard.KeyboardLayout.Key.*; + +public final class FallbackKeyboardLayout { + public static final ResourceLocation ID = CUtil.rl("fallback"); + + public static final KeyboardLayout QWERTY = KeyboardLayout.of(13.0f, + List.of( + k("§", "±"), + k("q"), + k("w"), + k("e"), + k("r"), + k("t"), + k("y"), + k("u"), + k("i"), + k("o"), + k("p"), + k(SpecialKey.Action.BACKSPACE, 2.0f, ControlifyBindings.GUI_ABSTRACT_ACTION_1) + ), + List.of( + k(SpecialKey.Action.TAB, 1f, null), + k("a"), + k("s"), + k("d"), + k("f"), + k("g"), + k("h"), + k("j"), + k("k"), + k("l"), + k("'", "\""), + k(SpecialKey.Action.ENTER, 2.0f, ControlifyBindings.GUI_ABSTRACT_ACTION_2) + ), + List.of( + k(SpecialKey.Action.SHIFT, 2f, ControlifyBindings.GUI_ABSTRACT_ACTION_3), + k("z"), + k("x"), + k("c"), + k("v"), + k("b"), + k("n"), + k("m"), + k(",", "."), + k("/", "\\"), + k(SpecialKey.Action.LEFT_ARROW, 1f, null), + k(SpecialKey.Action.RIGHT_ARROW, 1f, null) + ), + List.of( + k(SpecialKey.Action.UP_ARROW, 1f, null), + k(" ", 11f), + k(SpecialKey.Action.DOWN_ARROW, 1f, null) + ) + ); + + private static ShiftableKey k(SpecialKey.Action action, float width, @Nullable InputBindingSupplier shortcutBinding) { + return new ShiftableKey(new SpecialKey(action), width, Optional.ofNullable(shortcutBinding)); + } + private static ShiftableKey k(String string) { + return new ShiftableKey(new StringKey(string)); + } + private static ShiftableKey k(String regular, String shifted) { + return new ShiftableKey(new StringKey(regular), new StringKey(shifted)); + } + private static ShiftableKey k(String string, float width) { + return new ShiftableKey(new StringKey(string), width); + } +} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java index 39780715..a6c8cab9 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java @@ -1,6 +1,7 @@ package dev.isxander.controlify.screenkeyboard; import com.mojang.blaze3d.platform.InputConstants; +import dev.isxander.controlify.Controlify; import dev.isxander.controlify.api.bind.InputBinding; import dev.isxander.controlify.bindings.ControlifyBindings; import dev.isxander.controlify.controller.ControllerEntity; @@ -21,6 +22,7 @@ public class KeyWidget extends AbstractWidget implements ComponentProcessor, ScreenControllerEventListener { public static final ResourceLocation SPRITE = CUtil.rl("keyboard/key"); + public static final ResourceLocation SPRITE_PRESSED = CUtil.rl("keyboard/key_pressed"); private final KeyboardWidget keyboard; private final KeyboardLayout.ShiftableKey key; @@ -30,6 +32,8 @@ public class KeyWidget extends AbstractWidget implements ComponentProcessor, Scr private boolean shortcutPressed; private final HoldRepeatHelper holdRepeatHelper; + private boolean buttonPressed, mousePressed; + public KeyWidget(int x, int y, int width, int height, KeyboardLayout.ShiftableKey key, KeyboardWidget keyboard) { super(x, y, width, height, Component.literal("Key")); this.keyboard = keyboard; @@ -41,22 +45,27 @@ public KeyWidget(int x, int y, int width, int height, KeyboardLayout.ShiftableKe } public void renderKeyBackground(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { - Blit.sprite(graphics, SPRITE, getX() + 1, getY() + 1, getWidth() - 2, getHeight() - 2); + if (!this.isFocused()) { + // the call in overrideControllerButtons won't be triggered to un-press if the key is not focused + this.buttonPressed = false; + } - if (isHoveredOrFocused() || this.shortcutPressed) { - graphics.renderOutline(getX(), getY(), getWidth(), getHeight(), -1); - } else { + Blit.sprite(graphics, isVisuallyPressed() ? SPRITE_PRESSED : SPRITE, getX() + 1, getY() + 1, getWidth() - 2, getHeight() - 2); + + if (isHoveredOrFocused()) { + graphics.renderOutline(getX() - 1, getY() - 1, getWidth() + 2, getHeight() + 2, 0x80FFFFFF); + } else if (!shortcutPressed) { this.holdRepeatHelper.reset(); } } public void renderKeyForeground(GuiGraphics graphics, int mouseX, int mouseY, float deltaTick) { - Component label = this.keyboard.shifting ? this.shiftedLabel : this.unshiftedLabel; + Component label = this.keyboard.isShifted() ? this.shiftedLabel : this.unshiftedLabel; graphics.drawCenteredString( Minecraft.getInstance().font, label, getX() + getWidth() / 2, - getY() + getHeight() / 2 - 4, + getY() + getHeight() / 2 - 4 + (isVisuallyPressed() ? 2 : 0), 0xFFFFFFFF ); } @@ -68,7 +77,19 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo @Override public boolean overrideControllerButtons(ScreenProcessor screen, ControllerEntity controller) { - if (holdRepeatHelper.shouldAction(ControlifyBindings.GUI_PRESS.on(controller))) { + var guiPress = ControlifyBindings.GUI_PRESS.on(controller); + + // this means if you were holding the button down and navigated to this key, + // it would not show as pressed until you release and press again + if (guiPress.justPressed()) { + this.buttonPressed = true; + } else if (guiPress.justReleased()) { + this.buttonPressed = false; + } + + // prevent the press action if the button was navigated to whilst holding the button down + // the above visual state will not be pressed, so this should not trigger either + if (this.buttonPressed && holdRepeatHelper.shouldAction(guiPress)) { onPress(); holdRepeatHelper.onNavigate(); } @@ -93,38 +114,49 @@ public void onControllerInput(ControllerEntity controller) { @Override public boolean mouseClicked(double mouseX, double mouseY, int button /*? if >=1.21.9 {*/, boolean doubleClick /*?}*/) { if (isMouseOver(mouseX, mouseY)) { + this.mousePressed = true; onPress(); return true; } return false; } + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + this.mousePressed = false; + return super.mouseReleased(mouseX, mouseY, button); + } + private void onPress() { KeyboardLayout.Key key = this.getKey(); - KeyboardInputConsumer inputConsumer = this.keyboard.inputConsumer; + KeyboardInputConsumer inputConsumer = this.keyboard.getInputConsumer(); + + ScreenProcessor.playClackSound(); + + boolean wasShiftAction = false; switch (key) { - case KeyboardLayout.Key.StringKey stringKey -> { - stringKey.string().codePoints().forEach((codePoint) -> { - // guess the modifier based on the nature of the character - int modCapital = Character.isUpperCase(codePoint) ? GLFW.GLFW_MOD_SHIFT : 0; - int modifiers = modCapital; - - if (Character.isBmpCodePoint(codePoint)) { - inputConsumer.acceptChar((char) codePoint, modifiers); - } else if (Character.isValidCodePoint(codePoint)) { - inputConsumer.acceptChar(Character.highSurrogate(codePoint), modifiers); - inputConsumer.acceptChar(Character.lowSurrogate(codePoint), modifiers); - } - }); - } + case KeyboardLayout.Key.StringKey stringKey -> + insertText(stringKey.string(), inputConsumer); case KeyboardLayout.Key.CodeKey codeKey -> inputConsumer.acceptKeyCode(codeKey.keycode(), codeKey.scancode(), codeKey.modifier()); case KeyboardLayout.Key.SpecialKey specialKey -> { switch (specialKey.action()) { - case SHIFT -> this.keyboard.shifting = !this.keyboard.shifting; + case SHIFT -> { + if (!this.keyboard.isShiftLocked()) { + this.keyboard.setShifted(!this.keyboard.isShifted()); + } + wasShiftAction = true; + } + case SHIFT_LOCK -> { + boolean shiftLocked = !this.keyboard.isShiftLocked(); + this.keyboard.setShiftLocked(shiftLocked); + this.keyboard.setShifted(shiftLocked); + + wasShiftAction = true; + } case ENTER -> inputConsumer.acceptKeyCode(InputConstants.KEY_RETURN, 0, 0); case BACKSPACE -> inputConsumer.acceptKeyCode(InputConstants.KEY_BACKSPACE, 0, 0); @@ -133,13 +165,39 @@ private void onPress() { case RIGHT_ARROW -> inputConsumer.acceptKeyCode(InputConstants.KEY_RIGHT, 0, 0); case UP_ARROW -> inputConsumer.acceptKeyCode(InputConstants.KEY_UP, 0, 0); case DOWN_ARROW -> inputConsumer.acceptKeyCode(InputConstants.KEY_DOWN, 0, 0); + + case PASTE -> { + String clipboard = Minecraft.getInstance().keyboardHandler.getClipboard(); + insertText(clipboard, inputConsumer); + } + + default -> { + CUtil.LOGGER.warn("Unhandled code action: " + specialKey.action()); + } } } + + case KeyboardLayout.Key.ChangeLayoutKey changeLayoutKey -> { + ResourceLocation layoutId = changeLayoutKey.otherLayout(); + KeyboardLayoutWithId layoutWithId = Controlify.instance().keyboardLayoutManager().getLayout(layoutId); + this.keyboard.updateLayout(layoutWithId); + } } + + if (!wasShiftAction && this.keyboard.isShifted() && !this.keyboard.isShiftLocked()) { + // the key is shiftable if the key identity is different (i.e. it's a different key when shifted) + if (this.key.regular() != this.key.shifted()) { + this.keyboard.setShifted(false); + } + } + } + + public KeyboardLayout.Key getKey() { + return this.key.get(this.keyboard.isShifted()); } - private KeyboardLayout.Key getKey() { - return this.key.get(this.keyboard.shifting); + public boolean isVisuallyPressed() { + return this.buttonPressed || this.shortcutPressed || this.mousePressed; } @Override @@ -147,6 +205,21 @@ protected void updateWidgetNarration(NarrationElementOutput narrationElementOutp } + private static void insertText(String text, KeyboardInputConsumer inputConsumer) { + text.codePoints().forEach((codePoint) -> { + // guess the modifier based on the nature of the character + int modCapital = Character.isUpperCase(codePoint) ? GLFW.GLFW_MOD_SHIFT : 0; + int modifiers = modCapital; + + if (Character.isBmpCodePoint(codePoint)) { + inputConsumer.acceptChar((char) codePoint, modifiers); + } else if (Character.isValidCodePoint(codePoint)) { + inputConsumer.acceptChar(Character.highSurrogate(codePoint), modifiers); + inputConsumer.acceptChar(Character.lowSurrogate(codePoint), modifiers); + } + }); + } + private static Component createLabel(KeyboardLayout.ShiftableKey shiftableKey, boolean shift) { KeyboardLayout.Key key = shiftableKey.get(shift); diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java index 83ab666b..1574cf75 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java @@ -7,20 +7,23 @@ import com.mojang.serialization.DynamicOps; import com.mojang.serialization.codecs.RecordCodecBuilder; import dev.isxander.controlify.api.bind.InputBindingSupplier; +import dev.isxander.controlify.utils.codec.CExtraCodecs; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentSerialization; -import net.minecraft.util.ExtraCodecs; +import net.minecraft.resources.ResourceLocation; import net.minecraft.util.StringRepresentable; +import org.apache.commons.lang3.Validate; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Optional; import java.util.function.Function; import java.util.stream.Stream; -public record KeyboardLayout(float rowWidth, List> keys) { +public record KeyboardLayout(float width, List> keys) { public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( - Codec.floatRange(1, Float.MAX_VALUE).fieldOf("width").forGetter(KeyboardLayout::rowWidth), + Codec.floatRange(1, Float.MAX_VALUE).fieldOf("width").forGetter(KeyboardLayout::width), ShiftableKey.CODEC .listOf(1, Integer.MAX_VALUE) .listOf(1, Integer.MAX_VALUE) @@ -29,7 +32,7 @@ public record KeyboardLayout(float rowWidth, List> keys) { layout -> validateRowWidths(layout) ? DataResult.success(layout) - : DataResult.error(() -> "Row widths do not match the specified row width: " + layout.rowWidth()) + : DataResult.error(() -> "Row widths do not match the specified row width: " + layout.width()) ); public static boolean validateRowWidths(KeyboardLayout layout) { @@ -37,59 +40,72 @@ public static boolean validateRowWidths(KeyboardLayout layout) { .mapToDouble(row -> row.stream() .mapToDouble(ShiftableKey::width) .sum()) - .allMatch(rowWidth -> rowWidth == layout.rowWidth()); + .allMatch(rowWidth -> rowWidth == layout.width()); } - public record ShiftableKey(Key regular, Key shifted, Optional shortcutBinding) { + @SafeVarargs + public static KeyboardLayout of(float width, List... keys) { + var layout = new KeyboardLayout(width, List.of(keys)); + Validate.isTrue(validateRowWidths(layout), "All row widths do not match the specified row width: " + width); + return layout; + } + + public record ShiftableKey(Key regular, Key shifted, float width, Optional shortcutBinding) { + private static final Codec WIDTH_CODEC = Codec.floatRange(0.1f, Float.MAX_VALUE); + private static final Codec PAIR_CODEC = Codec.withAlternative( RecordCodecBuilder.create(instance -> instance.group( Key.CODEC.fieldOf("regular").forGetter(ShiftableKey::regular), Key.CODEC.optionalFieldOf("shifted").forGetter(k -> Optional.of(k.shifted)), + WIDTH_CODEC.optionalFieldOf("width", 1f).forGetter(ShiftableKey::width), InputBindingSupplier.CODEC.optionalFieldOf("shortcut").forGetter(ShiftableKey::shortcutBinding) - ).apply(instance, ShiftableKey::new)), - Key.CODEC.listOf(2, 2).xmap( - list -> new ShiftableKey(list.get(0), list.get(1), Optional.empty()), - key -> List.of(key.regular(), key.shifted()) - ) + ).apply(instance, ShiftableKey::fromCodec)), + CExtraCodecs.arrayPair(Key.CODEC, ShiftableKey::regular, ShiftableKey::shifted, ShiftableKey::new) ); public static Codec CODEC = Codec.either(Key.CODEC, PAIR_CODEC) .xmap( either -> either.map(ShiftableKey::new, Function.identity()), Either::right - ).validate(key -> key.validateWidths() - ? DataResult.success(key) - : DataResult.error(() -> "Regular and shifted keys have different widths: " + key) ); public ShiftableKey(Key key) { - this(key, key.createUppercase().orElse(key), Optional.empty()); + this(key, key.createUppercase()); } - public ShiftableKey(Key regular, Optional shifted, Optional shortcutBinding) { - this(regular, shifted.orElse(regular.createUppercase().orElse(regular)), shortcutBinding); + public ShiftableKey(Key key, float width) { + this(key, width, Optional.empty()); } - public Key get(boolean shifted) { - return shifted ? this.shifted : this.regular; + public ShiftableKey(Key key, float width, Optional shortcut) { + this(key, key.createUppercase(), width, shortcut); + } + + public ShiftableKey(Key regular, Key shifted) { + this(regular, shifted, 1f, Optional.empty()); } - public float width() { - return regular.width(); + private static ShiftableKey fromCodec(Key regular, Optional shifted, float width, Optional shortcut) { + return new ShiftableKey( + regular, + shifted.orElseGet(regular::createUppercase), + width, + shortcut + ); } - public boolean validateWidths() { - return regular().width() == shifted().width(); + public Key get(boolean shifted) { + return shifted ? this.shifted : this.regular; } } public sealed interface Key { - float width(); - Component displayName(); - default Optional createUppercase() { - return Optional.empty(); + @Nullable String identifier(); + + default Key createUppercase() { + return this; } Codec CODEC = new Codec<>() { @@ -99,12 +115,13 @@ public DataResult encode(Key input, DynamicOps ops, T prefix) { case StringKey stringKey -> StringKey.CODEC.encode(stringKey, ops, prefix); case CodeKey codeKey -> CodeKey.CODEC.encode(codeKey, ops, prefix); case SpecialKey specialKey -> SpecialKey.CODEC.encode(specialKey, ops, prefix); + case ChangeLayoutKey changeLayoutKey -> ChangeLayoutKey.CODEC.encode(changeLayoutKey, ops, prefix); }; } @Override public DataResult> decode(DynamicOps ops, T input) { - return Stream.of(StringKey.CODEC, CodeKey.CODEC, SpecialKey.CODEC) + return Stream.of(StringKey.CODEC, CodeKey.CODEC, SpecialKey.CODEC, ChangeLayoutKey.CODEC) .map(decoder -> decoder.decode(ops, input)) .map(r -> r.map(p -> p.mapFirst(t -> (Key) t))) .filter(DataResult::isSuccess) @@ -113,54 +130,65 @@ public DataResult> decode(DynamicOps ops, T input) { } }; - record StringKey(String string, float width) implements Key { - private static final Codec STR_CODEC = Codec.string(1, Integer.MAX_VALUE); - + record StringKey(String string, @Nullable Component manualDisplayName, @Nullable String identifier) implements Key { public static final Codec CODEC = Codec.withAlternative( RecordCodecBuilder.create(instance -> instance.group( - STR_CODEC.fieldOf("chars").forGetter(StringKey::string), - Codec.FLOAT.optionalFieldOf("width", 1.0f).forGetter(StringKey::width) - ).apply(instance, StringKey::new)), - STR_CODEC.xmap(StringKey::new, StringKey::string) + Codec.STRING.fieldOf("chars").forGetter(StringKey::string), + ComponentSerialization.CODEC.optionalFieldOf("display_name").forGetter(k -> Optional.of(k.displayName())), + Codec.STRING.optionalFieldOf("identifier").forGetter(k -> Optional.ofNullable(k.identifier)) + ).apply(instance, StringKey::fromCodec)), + Codec.STRING.xmap(StringKey::new, StringKey::string) ); public StringKey(String string) { - this(string, 1.0f); + this(string, null, null); + } + + private static StringKey fromCodec(String string, Optional displayName, Optional identifier) { + return new StringKey(string, displayName.orElse(null), identifier.orElse(null)); } @Override public Component displayName() { - return Component.literal(string); + return manualDisplayName != null ? manualDisplayName : Component.literal(string); } @Override - public Optional createUppercase() { - return Optional.of(new StringKey(string.toUpperCase(), width)); + public Key createUppercase() { + return new StringKey(string.toUpperCase(), manualDisplayName, identifier); } } - record CodeKey(int keycode, int scancode, int modifier, Component displayName, float width) implements Key { + record CodeKey(int keycode, int scancode, int modifier, Component displayName, @Nullable String identifier) implements Key { public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.INT.fieldOf("keycode").forGetter(CodeKey::keycode), Codec.INT.optionalFieldOf("scancode", 0).forGetter(CodeKey::scancode), Codec.INT.optionalFieldOf("modifier", 0).forGetter(CodeKey::modifier), ComponentSerialization.CODEC.fieldOf("display_name").forGetter(CodeKey::displayName), - Codec.FLOAT.optionalFieldOf("width", 1.0f).forGetter(CodeKey::width) - ).apply(instance, CodeKey::new)); + Codec.STRING.optionalFieldOf("identifier").forGetter(k -> Optional.ofNullable(k.identifier)) + ).apply(instance, CodeKey::fromCodec)); public CodeKey(int keycode, int scancode, int modifier, Component displayName) { - this(keycode, scancode, modifier, displayName, 1.0f); + this(keycode, scancode, modifier, displayName, null); + } + + private static CodeKey fromCodec(int keycode, int scancode, int modifier, Component displayName, Optional identifier) { + return new CodeKey(keycode, scancode, modifier, displayName, identifier.orElse(null)); } } - record SpecialKey(Action action, float width) implements Key { + record SpecialKey(Action action, @Nullable String identifier) implements Key { public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( StringRepresentable.fromEnum(Action::values).fieldOf("action").forGetter(SpecialKey::action), - Codec.FLOAT.optionalFieldOf("width", 1.0f).forGetter(SpecialKey::width) - ).apply(instance, SpecialKey::new)); + Codec.STRING.optionalFieldOf("identifier").forGetter(k -> Optional.ofNullable(k.identifier)) + ).apply(instance, SpecialKey::fromCodec)); public SpecialKey(Action action) { - this(action, 1.0f); + this(action, null); + } + + private static SpecialKey fromCodec(Action action, Optional identifier) { + return new SpecialKey(action, identifier.orElse(null)); } @Override @@ -170,13 +198,17 @@ public Component displayName() { public enum Action implements StringRepresentable { SHIFT("shift"), + SHIFT_LOCK("shift_lock"), ENTER("enter"), BACKSPACE("backspace"), TAB("tab"), LEFT_ARROW("left_arrow"), RIGHT_ARROW("right_arrow"), UP_ARROW("up_arrow"), - DOWN_ARROW("down_arrow"); + DOWN_ARROW("down_arrow"), + COPY_ALL("copy_all"), + PASTE("paste"), + PREVIOUS_LAYOUT("previous_layout"); private final String serialName; @@ -194,5 +226,17 @@ public Component displayName() { } } } + + record ChangeLayoutKey(ResourceLocation otherLayout, Component displayName, @Nullable String identifier) implements Key { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + ResourceLocation.CODEC.fieldOf("layout").forGetter(ChangeLayoutKey::otherLayout), + ComponentSerialization.CODEC.fieldOf("display_name").forGetter(ChangeLayoutKey::displayName), + Codec.STRING.optionalFieldOf("identifier").forGetter(k -> Optional.ofNullable(k.identifier)) + ).apply(instance, ChangeLayoutKey::fromCodec)); + + private static ChangeLayoutKey fromCodec(ResourceLocation layout, Component displayName, Optional identifier) { + return new ChangeLayoutKey(layout, displayName, identifier.orElse(null)); + } + } } } diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutManager.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutManager.java index d49b2f86..4a461fb1 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutManager.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutManager.java @@ -7,15 +7,14 @@ import dev.isxander.controlify.utils.CUtil; import dev.isxander.controlify.utils.log.ControlifyLogger; import net.minecraft.Util; -import net.minecraft.resources.FileToIdConverter; +import net.minecraft.client.Minecraft; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; import java.io.BufferedReader; -import java.util.Collections; import java.util.Map; -import java.util.Objects; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.stream.Collectors; @@ -23,15 +22,14 @@ public class KeyboardLayoutManager implements SimpleControlifyReloadListener { private static final String PREFIX = "keyboard_layout"; - private static final FileToIdConverter fileToIdConverter = FileToIdConverter.json(PREFIX); private static final ControlifyLogger LOGGER = CUtil.LOGGER.createSubLogger("KeyboardLayoutManager"); - private Map layouts = Map.of(); + private Map layouts = Map.of(); @Override public CompletableFuture load(ResourceManager manager, Executor executor) { return CompletableFuture.supplyAsync( - () -> fileToIdConverter.listMatchingResources(manager), + () -> listMatchingResources(manager), executor ).thenCompose(layoutMap -> { var futures = layoutMap.entrySet().stream() @@ -46,20 +44,20 @@ public CompletableFuture load(ResourceManager manager, Executor ex }); } - private CompletableFuture> loadLayout( + private CompletableFuture> loadLayout( ResourceLocation file, Resource resource, Executor executor ) { return CompletableFuture.supplyAsync(() -> { - ResourceLocation id = fileToIdConverter.fileToId(file); + KeyboardLayoutKey key = fileToKey(file); try (BufferedReader reader = resource.openAsReader()) { JsonElement json = JsonParser.parseReader(reader); KeyboardLayout layout = KeyboardLayout.CODEC.parse(JsonOps.INSTANCE, json) - .getOrThrow(reason -> new RuntimeException("Failed to parse keyboard layout " + id + ": " + reason)); + .getOrThrow(reason -> new RuntimeException("Failed to parse keyboard layout " + key + ": " + reason)); - return Map.entry(id, layout); + return Map.entry(key, layout); } catch (Exception e) { - throw new RuntimeException("Failed to read keyboard layout " + id + ": " + e.getMessage(), e); + throw new RuntimeException("Failed to read keyboard layout " + key + ": " + e.getMessage(), e); } }, executor); } @@ -72,12 +70,18 @@ public CompletableFuture apply(Preparations data, ResourceManager manager, }, executor); } - public Map getLayouts() { - return Collections.unmodifiableMap(layouts); + public KeyboardLayoutWithId getLayout(ResourceLocation layoutId, String languageCode) { + var key = new KeyboardLayoutKey(languageCode, layoutId); + + return Optional.ofNullable(this.layouts.get(key)) + .or(() -> Optional.ofNullable(this.layouts.get(key.withDefaultLanguage()))) + .map(layout -> new KeyboardLayoutWithId(layout, layoutId)) + .orElse(KeyboardLayouts.fallback()); } - public KeyboardLayout getLayout(ResourceLocation id) { - return Objects.requireNonNull(layouts.get(id)); + public KeyboardLayoutWithId getLayout(ResourceLocation layout) { + String currentLanguage = Minecraft.getInstance().getLanguageManager().getSelected(); + return getLayout(layout, currentLanguage); } @Override @@ -85,5 +89,40 @@ public ResourceLocation getReloadId() { return CUtil.rl("keyboard_layout"); } - public record Preparations(Map layouts) {} + private static ResourceLocation keyToFile(KeyboardLayoutKey key) { + return key.layoutId().withPath(PREFIX + "/" + key.layoutId().getPath() + "/" + key.languageCode() + ".json"); + } + + private static KeyboardLayoutKey fileToKey(ResourceLocation file) { + try { + var components = file.getPath().split("/"); + var layoutPath = components[1]; + var languageCodeWithExt = components[2]; + var languageCode = languageCodeWithExt.substring(0, languageCodeWithExt.lastIndexOf('.')); + var layoutId = file.withPath(layoutPath); + + return new KeyboardLayoutKey(languageCode, layoutId); + } catch (IndexOutOfBoundsException e) { + throw new IllegalArgumentException("Invalid file path. Expected format: keyboard_layout//.json, but got " + file.getPath(), e); + } + + } + + private static Map listMatchingResources(ResourceManager resourceManager) { + return resourceManager.listResources(PREFIX, path -> path.getPath().endsWith(".json")); + } + + public record Preparations(Map layouts) {} + + private record KeyboardLayoutKey(String languageCode, ResourceLocation layoutId) { + public static final String DEFAULT_LANGUAGE = "en_us"; + + public KeyboardLayoutKey withLanguage(String languageCode) { + return new KeyboardLayoutKey(languageCode, this.layoutId); + } + + public KeyboardLayoutKey withDefaultLanguage() { + return new KeyboardLayoutKey(DEFAULT_LANGUAGE, this.layoutId); + } + } } diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutWithId.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutWithId.java new file mode 100644 index 00000000..233e6fe4 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutWithId.java @@ -0,0 +1,6 @@ +package dev.isxander.controlify.screenkeyboard; + +import net.minecraft.resources.ResourceLocation; + +public record KeyboardLayoutWithId(KeyboardLayout layout, ResourceLocation id) { +} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayouts.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayouts.java index 8c028353..5cc60d6d 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayouts.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayouts.java @@ -8,9 +8,13 @@ public final class KeyboardLayouts { public static final ResourceLocation CHAT = CUtil.rl("chat"); - public static KeyboardLayout chat() { + public static KeyboardLayoutWithId chat() { return Controlify.instance().keyboardLayoutManager().getLayout(CHAT); } + public static KeyboardLayoutWithId fallback() { + return new KeyboardLayoutWithId(FallbackKeyboardLayout.QWERTY, FallbackKeyboardLayout.ID); + } + private KeyboardLayouts() {} } diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java index a505baa2..27693442 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java @@ -1,5 +1,6 @@ package dev.isxander.controlify.screenkeyboard; +import com.google.common.collect.ImmutableList; import dev.isxander.controlify.screenop.ScreenProcessorProvider; import dev.isxander.controlify.utils.render.Blit; import net.minecraft.client.gui.ComponentPath; @@ -11,53 +12,72 @@ import net.minecraft.client.gui.navigation.FocusNavigationEvent; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import java.util.function.Predicate; public class KeyboardWidget extends AbstractWidget implements ContainerEventHandler { - private final KeyboardLayout layout; - final KeyboardInputConsumer inputConsumer; + private final ResourceLocation currentLayout; + private KeyboardInputConsumer inputConsumer; - private final List keys; + private List keys = ImmutableList.of(); - boolean shifting; + private boolean shifted, shiftLocked; - private @Nullable GuiEventListener focused; + private @Nullable KeyWidget focused; private boolean isDragging; private final Screen containingScreen; - public KeyboardWidget(int x, int y, int width, int height, KeyboardLayout layout, KeyboardInputConsumer inputConsumer, Screen containingScreen) { + public KeyboardWidget(int x, int y, int width, int height, KeyboardLayoutWithId layout, KeyboardInputConsumer inputConsumer, Screen containingScreen) { super(x, y, width, height, Component.literal("On-Screen Keyboard")); - this.layout = layout; this.inputConsumer = inputConsumer; this.containingScreen = containingScreen; + this.currentLayout = layout.id(); + this.updateLayout(layout.layout(), "initial_focus", null); + } + + public void updateLayout(KeyboardLayoutWithId layout) { + ResourceLocation oldLayoutId = this.getCurrentLayoutId(); + @Nullable String oldIdentifier = Optional.ofNullable(getFocused()) + .map(k -> k.getKey().identifier()) + .orElse(null); + + this.updateLayout(layout.layout(), oldIdentifier, oldLayoutId); + } - int keyCount = this.layout.keys().stream() + public void updateLayout(KeyboardLayout layout, @Nullable String identifierToFocus, @Nullable ResourceLocation oldLayoutChangerToFocus) { + this.arrangeKeys(layout); + + findKey( + identifierToFocus != null, + k -> Objects.equals(k.getKey().identifier(), identifierToFocus) + ).or(() -> findKey( + oldLayoutChangerToFocus != null, + k -> k.getKey() instanceof KeyboardLayout.Key.ChangeLayoutKey changeLayoutKey && changeLayoutKey.otherLayout().equals(oldLayoutChangerToFocus)) + ); + } + + private void arrangeKeys(KeyboardLayout layout) { + int keyCount = layout.keys().stream() .mapToInt(List::size) .sum(); this.keys = new ArrayList<>(keyCount); - this.arrangeKeys(); - System.out.println(layout); - } - private void arrangeKeys() { - this.keys.clear(); + float unitWidth = this.getWidth() / layout.width(); + float keyHeight = (float) this.getHeight() / layout.keys().size(); - float unitWidth = (float) this.getWidth() / this.layout.rowWidth(); - int keyHeight = (int)((float) this.getHeight() / this.layout.keys().size()); - - int y = this.getY(); - for (List row : this.layout.keys()) { - int x = this.getX(); + float y = this.getY(); + for (List row : layout.keys()) { + float x = this.getX(); for (KeyboardLayout.ShiftableKey key : row) { - int keyWidth = (int) (key.regular().width() * unitWidth); + float keyWidth = key.width() * unitWidth; var keyWidget = new KeyWidget( - x, y, keyWidth, keyHeight, + (int) x, (int) y, (int) keyWidth, (int) keyHeight, key, this ); ScreenProcessorProvider.provide(this.containingScreen).addEventListener(keyWidget); @@ -97,9 +117,47 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo }); } + public void setShifted(boolean shifted) { + this.shifted = shifted; + } + + public boolean isShifted() { + return shifted; + } + + public void setShiftLocked(boolean shiftLocked) { + this.shiftLocked = shiftLocked; + } + + public boolean isShiftLocked() { + return shiftLocked; + } + + public void setInputConsumer(KeyboardInputConsumer inputConsumer) { + this.inputConsumer = inputConsumer; + } + + public KeyboardInputConsumer getInputConsumer() { + return inputConsumer; + } + + public ResourceLocation getCurrentLayoutId() { + return this.currentLayout; + } + + private Optional findKey(boolean skipSearch, Predicate predicate) { + if (skipSearch) { + return Optional.empty(); + } + + return this.keys.stream() + .filter(predicate) + .findFirst(); + } + @Override public @NotNull List children() { - return this.keys; + return Collections.unmodifiableList(keys); } @Override @@ -118,12 +176,16 @@ public void setDragging(boolean dragging) { } @Override - public @Nullable GuiEventListener getFocused() { + public @Nullable KeyWidget getFocused() { return focused; } @Override public void setFocused(@Nullable GuiEventListener focused) { + if (focused != null && (!(focused instanceof KeyWidget) || !this.keys.contains(focused))) { + throw new IllegalArgumentException("Focused widget must be a KeyWidget in this KeyboardWidget"); + } + if (this.focused != null) { this.focused.setFocused(false); } @@ -132,7 +194,7 @@ public void setFocused(@Nullable GuiEventListener focused) { focused.setFocused(true); } - this.focused = focused; + this.focused = (KeyWidget) focused; } @Nullable diff --git a/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java b/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java index a020ec9b..be47825b 100644 --- a/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java @@ -158,8 +158,9 @@ protected void handleComponentNavigation(ControllerEntity controller) { controller.input().ifPresent(InputComponent::notifyGuiPressOutputsOfNavigate); - if (Controlify.instance().config().globalSettings().uiSounds) - minecraft.getSoundManager().play(SimpleSoundInstance.forUI(ControlifyClientSounds.SCREEN_FOCUS_CHANGE.get(), 1.0F)); + if (Controlify.instance().config().globalSettings().extraUiSounds) { + playFocusChangeSound(); + } controller.hdHaptics().ifPresent(haptics -> haptics.playHaptic(HapticEffects.NAVIGATE)); var newFocusTree = getFocusTree(); @@ -310,4 +311,8 @@ protected final Optional getWidget(String translationKey) { public static void playClackSound() { minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); } + + public static void playFocusChangeSound() { + minecraft.getSoundManager().play(SimpleSoundInstance.forUI(ControlifyClientSounds.SCREEN_FOCUS_CHANGE.get(), 1.0F)); + } } diff --git a/src/main/java/dev/isxander/controlify/utils/codec/CExtraCodecs.java b/src/main/java/dev/isxander/controlify/utils/codec/CExtraCodecs.java new file mode 100644 index 00000000..ffc01b88 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/utils/codec/CExtraCodecs.java @@ -0,0 +1,59 @@ +package dev.isxander.controlify.utils.codec; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.MapEncoder; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; + +public final class CExtraCodecs { + + public static MapCodec fuzzyMap( + List> codecs, + Function> encoderGetter) { + return new FuzzyMapCodec<>(codecs, encoderGetter); + } + + public static Codec> set(Codec elementCodec, int minSize, int maxSize) { + return new SetCodec<>(elementCodec, minSize, maxSize); + } + + public static Codec> set(Codec elementCodec) { + return new SetCodec<>(elementCodec, 0, Integer.MAX_VALUE); + } + + public static MapCodec strictEitherMap( + String typedKeyName, MapCodec typed, MapCodec fuzzy, boolean typedEncode + ) { + return new StrictEitherMapCodec<>(typedKeyName, typed, fuzzy, typedEncode); + } + + public static MapCodec<@Nullable T> nullableField(Codec codec, String fieldName) { + return codec.optionalFieldOf(fieldName).xmap( + opt -> opt.orElse(null), + Optional::ofNullable + ); + } + + public static Codec> arrayPair(Codec elementCodec) { + return elementCodec.listOf(2, 2).xmap( + list -> Pair.of(list.get(0), list.get(1)), + pair -> List.of(pair.getFirst(), pair.getSecond()) + ); + } + + public static Codec arrayPair(Codec elementCodec, Function firstGetter, Function secondGetter, BiFunction constructor) { + return arrayPair(elementCodec).xmap( + pair -> constructor.apply(pair.getFirst(), pair.getSecond()), + obj -> Pair.of(firstGetter.apply(obj), secondGetter.apply(obj)) + ); + } + + private CExtraCodecs() {} +} diff --git a/src/main/java/dev/isxander/controlify/utils/codec/SetCodec.java b/src/main/java/dev/isxander/controlify/utils/codec/SetCodec.java index ce3db7cb..da2eb9b1 100644 --- a/src/main/java/dev/isxander/controlify/utils/codec/SetCodec.java +++ b/src/main/java/dev/isxander/controlify/utils/codec/SetCodec.java @@ -11,10 +11,6 @@ public record SetCodec(Codec elementCodec, int minSize, int maxSize) implements Codec> { - public SetCodec(final Codec elementCodec) { - this(elementCodec, 0, Integer.MAX_VALUE); - } - private DataResult createTooShortError(final int size) { return DataResult.error(() -> "Set is too short: " + size + ", expected range [" + minSize + "-" + maxSize + "]"); } diff --git a/src/main/resources/assets/controlify/keyboard_layout/chat.json b/src/main/resources/assets/controlify/keyboard_layout/chat.json deleted file mode 100644 index b76b7242..00000000 --- a/src/main/resources/assets/controlify/keyboard_layout/chat.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "width": 10, - "keys": [ - [ ["q", "qwerty!"], "w", "e", "r", "t", "y", "u", "i", "o", "p" ], - [ {"regular": {"action": "shift"}, "shortcut": "controlify:gui_abstract_action_3"}, "a", "s", "d", "f", "g", "h", "j", "k", "l" ], - [ " ", " ", "z", "x", "c", "v", "b", "n", "m", " " ] - ] -} diff --git a/src/main/resources/assets/controlify/keyboard_layout/chat/en_us.json b/src/main/resources/assets/controlify/keyboard_layout/chat/en_us.json new file mode 100644 index 00000000..e6cb8ccc --- /dev/null +++ b/src/main/resources/assets/controlify/keyboard_layout/chat/en_us.json @@ -0,0 +1,80 @@ +{ + "width": 16, + "keys": [ + [ + { "regular": " ", "width": 1 }, + ["`", "~"], + ["1", "!"], + ["2", "@"], + ["3", "#"], + ["4", "$"], + ["5", "%"], + ["6", "^"], + ["7", "&"], + ["8", "*"], + ["9", "("], + ["0", ")"], + ["-", "_"], + ["=", "+"], + { "regular": { "action": "backspace"}, "shortcut": "controlify:gui_abstract_action_1", "width": 2.0 } + ], + [ + { "regular": " ", "width": 2 }, + "q", + "w", + "e", + "r", + "t", + "y", + "u", + "i", + "o", + "p", + ["[", "{"], + ["]", "}"], + ["\\", "|"], + " " + ], + [ + { "regular": { "action": "shift_lock" }, "width": 2.0 }, + "a", + "s", + "d", + "f", + "g", + "h", + "j", + "k", + "l", + [";", ":"], + ["'", "\""], + { "regular": { "action": "enter", "shortcut": "controlify:gui_abstract_action_1" }, "width": 3.0 } + ], + [ + { "regular": { "action": "shift" }, "shortcut": "controlify:gui_abstract_action_3", "width": 2.0 }, + "z", + "x", + "c", + "v", + "b", + "n", + "m", + [",", "<"], + [".", ">"], + ["/", "?"], + { + "regular": { "action": "paste" }, + "shifted": { "action": "copy_all" }, + "width": 2.0 + }, + { "action": "left_arrow" }, + { "action": "right_arrow" } + ], + [ + { "regular": { "layout": "controlify:emoji", "display_name": "☺" }, "width": 2.0 }, + { "regular": { "chars": " ", "display_name": "Space" }, "shortcut": "controlify:gui_abstract_action_2", "width": 12.0 }, + { "action": "up_arrow" }, + { "action": "down_arrow" } + ] + ] +} diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index d77d42f5..7337a433 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -583,5 +583,18 @@ "controlify.extra_pack.legacy_console.name": "Legacy Console", "controlify.extra_pack.legacy_console.desc": "Controlify - sets bind defaults/glyphs.", + "controlify.keyboard.special.shift": "Shift", + "controlify.keyboard.special.shift_lock": "Shift Lock", + "controlify.keyboard.special.enter": "Enter", + "controlify.keyboard.special.backspace": "Del", + "controlify.keyboard.special.tab": "Tab", + "controlify.keyboard.special.left_arrow": "←", + "controlify.keyboard.special.right_arrow": "→", + "controlify.keyboard.special.up_arrow": "↑", + "controlify.keyboard.special.down_arrow": "↓", + "controlify.keyboard.special.copy_all": "Copy All", + "controlify.keyboard.special.paste": "Paste", + "controlify.keyboard.special.previous_layout": "Previous Layout", + "controlify.tutorial.look.description": "Use your mouse or controller to turn" } diff --git a/src/main/resources/assets/controlify/textures/gui/sprites/keyboard/key_pressed.png b/src/main/resources/assets/controlify/textures/gui/sprites/keyboard/key_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..489c8109b7bf851beeae116b74b3f4c480b1f461 GIT binary patch literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^azHG>!3HGXX8P_2QjEnx?oJHr&dIz4a$-DP978-h z-(K4&*kHiJ?9iD!E$87&+os(<6M21@E!6}>f|ASYUmbbJcCyV(r9m#) Date: Thu, 7 Aug 2025 16:19:12 +0100 Subject: [PATCH 03/12] a lot more stuff! --- .github/README.md | 2 +- docs/architecture/mod-comparison.mdx | 2 +- docs/resource-packs/guides.mdx | 6 +- .../screenkeyboard/ChatScreenMixin.java | 63 +++-- .../FallbackKeyboardLayout.java | 36 +-- .../screenkeyboard/InputTarget.java | 24 ++ .../controlify/screenkeyboard/KeyWidget.java | 123 ++++++-- .../screenkeyboard/KeyboardInputConsumer.java | 7 - .../screenkeyboard/KeyboardLayout.java | 264 ++++++++++++------ .../screenkeyboard/KeyboardWidget.java | 59 ++-- .../screenkeyboard/MixinInputTarget.java | 45 +++ .../controlify/screenop/ScreenProcessor.java | 7 +- .../compat/vanilla/ChatScreenProcessor.java | 16 ++ 13 files changed, 476 insertions(+), 178 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/InputTarget.java delete mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardInputConsumer.java create mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/MixinInputTarget.java create mode 100644 src/main/java/dev/isxander/controlify/screenop/compat/vanilla/ChatScreenProcessor.java diff --git a/.github/README.md b/.github/README.md index ccdb4192..b142ef0b 100644 --- a/.github/README.md +++ b/.github/README.md @@ -160,4 +160,4 @@ A few features in various points in the horizon are: ## Backports? This mod is only and will only be available for **1.19.4** and above, this is because in 1.19.4, Mojang -introduced arrow key navigation which was easily ported to controller, below 1.19.4, this is not possible. +introduced arrow keyFunction navigation which was easily ported to controller, below 1.19.4, this is not possible. diff --git a/docs/architecture/mod-comparison.mdx b/docs/architecture/mod-comparison.mdx index af494e4a..98ee5b56 100644 --- a/docs/architecture/mod-comparison.mdx +++ b/docs/architecture/mod-comparison.mdx @@ -13,7 +13,7 @@ exhaustive, and there are many more features that are not listed here.** | **Open source** | ✅ Yes | ✅ Yes | ✅ Yes | ⛔ No | | **Library used** | SDL 3.x / GLFW | GLFW | SDL 2.x / GLFW | | | **Custom Screen Compatibility** | Convenient APIs to hook into controller support directly from `Screen` implementation. | No API. Sometimes necessary to mixin into Midnight Controls and edge-case code required. | ⛔ | ⛔ | -| **Screen Navigation** | 4-axis navigation, emulating arrow key navigation with optional cursor emulation | 4-axis navigation, emulating arrow key navigation | Cursor emulation only. | 2-axis tab-key emulation | +| **Screen Navigation** | 4-axis navigation, emulating arrow keyFunction navigation with optional cursor emulation | 4-axis navigation, emulating arrow keyFunction navigation | Cursor emulation only. | 2-axis tab-keyFunction emulation | | **Controller rumble** | ✅ | ⛔ | ✅ | ⛔ | | **In-game button guide** | ✅ Extensible by 3rd party mods | Hardcoded buttons and positions | Hardcoded buttons and positions | ⛔ | | **Input latency** | 20Hz - follows Minecraft's game loop rate | 1000Hz for axes, 20Hz for buttons | Unknown | Unknown | diff --git a/docs/resource-packs/guides.mdx b/docs/resource-packs/guides.mdx index 0e1a80fc..f8c7dfe5 100644 --- a/docs/resource-packs/guides.mdx +++ b/docs/resource-packs/guides.mdx @@ -59,7 +59,7 @@ When the _rule_ applies, it shows the text "Jump" and the glyph for the `control As well as literal text such as `"Jump"`, Controlify supports the Minecraft [text component format](https://minecraft.wiki/w/Text_component_format) which allows you to use translations and styling. -For example, you can use `"then": {"translate": "mypack.jump"}` to use a translation key sourced from your pack's language files. +For example, you can use `"then": {"translate": "mypack.jump"}` to use a translation keyFunction sourced from your pack's language files. A rule can be displayed either on the `left` or `right` side of the screen, this is defined by the `"where"` field. @@ -134,10 +134,10 @@ These facts are available in all domains. | `controlify:in_water` | When the player is touching water. | | `controlify:under_water` | When the player has their eyes underwater. | | `controlify:in_lava` | When the player is touching lava. | -| `controlify:sneaking` | When the player is attempting to sneak (pressing the sneak key, or it is toggled on). | +| `controlify:sneaking` | When the player is attempting to sneak (pressing the sneak keyFunction, or it is toggled on). | | `controlify:is_toggle_sneak` | When the player is using toggle sneak (does not mean it is currently toggled on). | | `controlify:is_toggle_sprint` | When the player is using toggle sprint (does not mean it is currently toggled on). | -| `controlify:sprinting` | When the player is attempting to sprint (pressing the sprint key, or it is toggled on). | +| `controlify:sprinting` | When the player is attempting to sprint (pressing the sprint keyFunction, or it is toggled on). | | `controlify:input_moving` | When the player is applying movement input—even if the player is not physically moving, if they're trying to, this fact goes. | | `controlify:is_spectator` | When the player is in spectator mode. | | `controlify:is_creative` | When the player is in creative mode. | diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java index 0ac5d673..68f2052f 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java @@ -1,12 +1,10 @@ package dev.isxander.controlify.mixins.feature.screenkeyboard; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.mojang.blaze3d.platform.InputConstants; import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.controller.keyboard.NativeKeyboardComponent; -import dev.isxander.controlify.screenkeyboard.ChatKeyboardDucky; -import dev.isxander.controlify.screenkeyboard.KeyboardInputConsumer; -import dev.isxander.controlify.screenkeyboard.KeyboardLayouts; -import dev.isxander.controlify.screenkeyboard.KeyboardWidget; +import dev.isxander.controlify.screenkeyboard.*; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.screens.ChatScreen; @@ -20,10 +18,11 @@ import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.List; import java.util.Optional; @Mixin(ChatScreen.class) -public abstract class ChatScreenMixin extends Screen implements ChatKeyboardDucky { +public abstract class ChatScreenMixin extends Screen implements MixinInputTarget, ChatKeyboardDucky { @Unique private KeyboardWidget keyboard; @Unique @@ -54,17 +53,7 @@ private void addKeyboard(CallbackInfo ci) { } else { this.shiftChatAmt = 0.5f; int keyboardHeight = (int) (this.height * this.shiftChatAmt); - this.addRenderableWidget(keyboard = new KeyboardWidget(0, this.height - keyboardHeight, this.width, keyboardHeight, KeyboardLayouts.chat(), new KeyboardInputConsumer() { - @Override - public void acceptChar(char ch, int modifiers) { - input.charTyped(ch, modifiers); - } - - @Override - public void acceptKeyCode(int keycode, int scancode, int modifiers) { - input.keyPressed(keycode, scancode, modifiers); - } - }, (ChatScreen) (Object) this)); + this.addRenderableWidget(keyboard = new KeyboardWidget(0, this.height - keyboardHeight, this.width, keyboardHeight, KeyboardLayouts.chat(), this, (ChatScreen) (Object) this)); } }); } @@ -93,4 +82,46 @@ private int modifyMaxSuggestionCount(int count) { public float controlify$keyboardShiftAmount() { return this.shiftChatAmt; } + + + + @Override + public boolean controlify$supportsCharInput() { + return true; + } + + @Override + public boolean controlify$acceptChar(char ch, int modifiers) { + this.input.charTyped(ch, modifiers); + return true; + } + + @Override + public boolean controlify$supportsKeyCodeInput() { + return true; + } + + @Override + public boolean controlify$acceptKeyCode(int keycode, int scancode, int modifiers) { + boolean bypassInput = List.of( + InputConstants.KEY_RETURN, + InputConstants.KEY_ESCAPE + ).contains(keycode); + + if (bypassInput) { + return ((ChatScreen) (Object) this).keyPressed(keycode, scancode, modifiers); + } + return this.input.keyPressed(keycode, scancode, modifiers); + } + + @Override + public boolean controlify$supportsCopying() { + return true; + } + + @Override + public boolean controlify$copy() { + minecraft.keyboardHandler.setClipboard(this.input.getValue()); + return true; + } } diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/FallbackKeyboardLayout.java b/src/main/java/dev/isxander/controlify/screenkeyboard/FallbackKeyboardLayout.java index 6486d4d2..d8bf7e12 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/FallbackKeyboardLayout.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/FallbackKeyboardLayout.java @@ -9,8 +9,8 @@ import java.util.List; import java.util.Optional; -import static dev.isxander.controlify.screenkeyboard.KeyboardLayout.ShiftableKey; -import static dev.isxander.controlify.screenkeyboard.KeyboardLayout.Key.*; +import static dev.isxander.controlify.screenkeyboard.KeyboardLayout.Key; +import static dev.isxander.controlify.screenkeyboard.KeyboardLayout.KeyFunction.*; public final class FallbackKeyboardLayout { public static final ResourceLocation ID = CUtil.rl("fallback"); @@ -28,10 +28,10 @@ public final class FallbackKeyboardLayout { k("i"), k("o"), k("p"), - k(SpecialKey.Action.BACKSPACE, 2.0f, ControlifyBindings.GUI_ABSTRACT_ACTION_1) + k(SpecialFunc.Action.BACKSPACE, 2.0f, ControlifyBindings.GUI_ABSTRACT_ACTION_1) ), List.of( - k(SpecialKey.Action.TAB, 1f, null), + k(SpecialFunc.Action.TAB, 1f, null), k("a"), k("s"), k("d"), @@ -42,10 +42,10 @@ public final class FallbackKeyboardLayout { k("k"), k("l"), k("'", "\""), - k(SpecialKey.Action.ENTER, 2.0f, ControlifyBindings.GUI_ABSTRACT_ACTION_2) + k(SpecialFunc.Action.ENTER, 2.0f, ControlifyBindings.GUI_ABSTRACT_ACTION_2) ), List.of( - k(SpecialKey.Action.SHIFT, 2f, ControlifyBindings.GUI_ABSTRACT_ACTION_3), + k(SpecialFunc.Action.SHIFT, 2f, ControlifyBindings.GUI_ABSTRACT_ACTION_3), k("z"), k("x"), k("c"), @@ -55,26 +55,26 @@ public final class FallbackKeyboardLayout { k("m"), k(",", "."), k("/", "\\"), - k(SpecialKey.Action.LEFT_ARROW, 1f, null), - k(SpecialKey.Action.RIGHT_ARROW, 1f, null) + k(SpecialFunc.Action.LEFT_ARROW, 1f, null), + k(SpecialFunc.Action.RIGHT_ARROW, 1f, null) ), List.of( - k(SpecialKey.Action.UP_ARROW, 1f, null), + k(SpecialFunc.Action.UP_ARROW, 1f, null), k(" ", 11f), - k(SpecialKey.Action.DOWN_ARROW, 1f, null) + k(SpecialFunc.Action.DOWN_ARROW, 1f, null) ) ); - private static ShiftableKey k(SpecialKey.Action action, float width, @Nullable InputBindingSupplier shortcutBinding) { - return new ShiftableKey(new SpecialKey(action), width, Optional.ofNullable(shortcutBinding)); + private static Key k(SpecialFunc.Action action, float width, @Nullable InputBindingSupplier shortcutBinding) { + return new Key(new SpecialFunc(action), width, Optional.ofNullable(shortcutBinding)); } - private static ShiftableKey k(String string) { - return new ShiftableKey(new StringKey(string)); + private static Key k(String string) { + return new Key(new StringFunc(string)); } - private static ShiftableKey k(String regular, String shifted) { - return new ShiftableKey(new StringKey(regular), new StringKey(shifted)); + private static Key k(String regular, String shifted) { + return new Key(new StringFunc(regular), new StringFunc(shifted)); } - private static ShiftableKey k(String string, float width) { - return new ShiftableKey(new StringKey(string), width); + private static Key k(String string, float width) { + return new Key(new StringFunc(string), width); } } diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/InputTarget.java b/src/main/java/dev/isxander/controlify/screenkeyboard/InputTarget.java new file mode 100644 index 00000000..84d65107 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/InputTarget.java @@ -0,0 +1,24 @@ +package dev.isxander.controlify.screenkeyboard; + +public interface InputTarget { + default boolean supportsCharInput() { + return false; + } + default boolean acceptChar(char ch, int modifiers) { + return false; + } + + default boolean supportsKeyCodeInput() { + return false; + } + default boolean acceptKeyCode(int keycode, int scancode, int modifiers) { + return false; + } + + default boolean supportsCopying() { + return false; + } + default boolean copy() { + return false; + } +} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java index a6c8cab9..25d3f5d0 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java @@ -20,31 +20,38 @@ import net.minecraft.resources.ResourceLocation; import org.lwjgl.glfw.GLFW; +import java.util.Optional; + public class KeyWidget extends AbstractWidget implements ComponentProcessor, ScreenControllerEventListener { public static final ResourceLocation SPRITE = CUtil.rl("keyboard/key"); public static final ResourceLocation SPRITE_PRESSED = CUtil.rl("keyboard/key_pressed"); private final KeyboardWidget keyboard; - private final KeyboardLayout.ShiftableKey key; + private final KeyboardLayout.Key key; - private final Component unshiftedLabel, shiftedLabel; + private final Component regularLabel, shiftedLabel; + private final boolean supportsRegular, supportsShifted; private boolean shortcutPressed; private final HoldRepeatHelper holdRepeatHelper; private boolean buttonPressed, mousePressed; - public KeyWidget(int x, int y, int width, int height, KeyboardLayout.ShiftableKey key, KeyboardWidget keyboard) { + public KeyWidget(int x, int y, int width, int height, KeyboardLayout.Key key, KeyboardWidget keyboard) { super(x, y, width, height, Component.literal("Key")); this.keyboard = keyboard; this.key = key; this.holdRepeatHelper = new HoldRepeatHelper(10, 2); - this.unshiftedLabel = createLabel(key, false); + this.regularLabel = createLabel(key, false); this.shiftedLabel = createLabel(key, true); + this.supportsRegular = supportsAction(false); + this.supportsShifted = supportsAction(true); } public void renderKeyBackground(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + this.active = this.supportsAction(); + if (!this.isFocused()) { // the call in overrideControllerButtons won't be triggered to un-press if the key is not focused this.buttonPressed = false; @@ -57,10 +64,15 @@ public void renderKeyBackground(GuiGraphics graphics, int mouseX, int mouseY, fl } else if (!shortcutPressed) { this.holdRepeatHelper.reset(); } + + if (!this.active) { + // gray out the key if it does not support the action + graphics.fill(getX() + 1, getY() + 1, getX() + getWidth() - 1, getY() + getHeight() - 1, 0x30000000); + } } public void renderKeyForeground(GuiGraphics graphics, int mouseX, int mouseY, float deltaTick) { - Component label = this.keyboard.isShifted() ? this.shiftedLabel : this.unshiftedLabel; + Component label = this.keyboard.isShifted() ? this.shiftedLabel : this.regularLabel; graphics.drawCenteredString( Minecraft.getInstance().font, label, @@ -94,7 +106,7 @@ public boolean overrideControllerButtons(ScreenProcessor screen, ControllerEn holdRepeatHelper.onNavigate(); } - return true; + return false; } @Override @@ -128,25 +140,28 @@ public boolean mouseReleased(double mouseX, double mouseY, int button) { } private void onPress() { - KeyboardLayout.Key key = this.getKey(); - KeyboardInputConsumer inputConsumer = this.keyboard.getInputConsumer(); + KeyboardLayout.KeyFunction keyFunction = this.getKeyFunction(); + InputTarget inputConsumer = this.keyboard.getInputTarget(); ScreenProcessor.playClackSound(); boolean wasShiftAction = false; - switch (key) { - case KeyboardLayout.Key.StringKey stringKey -> + switch (keyFunction) { + case KeyboardLayout.KeyFunction.StringFunc stringKey -> insertText(stringKey.string(), inputConsumer); - case KeyboardLayout.Key.CodeKey codeKey -> - inputConsumer.acceptKeyCode(codeKey.keycode(), codeKey.scancode(), codeKey.modifier()); + case KeyboardLayout.KeyFunction.CodeFunc codeKey -> + codeKey.codes().forEach(code -> inputConsumer.acceptKeyCode(code.keycode(), code.scancode(), code.modifier())); - case KeyboardLayout.Key.SpecialKey specialKey -> { + case KeyboardLayout.KeyFunction.SpecialFunc specialKey -> { switch (specialKey.action()) { case SHIFT -> { if (!this.keyboard.isShiftLocked()) { this.keyboard.setShifted(!this.keyboard.isShifted()); + } else { + this.keyboard.setShifted(false); + this.keyboard.setShiftLocked(false); } wasShiftAction = true; } @@ -170,6 +185,13 @@ private void onPress() { String clipboard = Minecraft.getInstance().keyboardHandler.getClipboard(); insertText(clipboard, inputConsumer); } + case COPY_ALL -> { + inputConsumer.copy(); + } + + case PREVIOUS_LAYOUT -> { + this.keyboard.getPreviousLayoutId().ifPresent(this::changeLayout); + } default -> { CUtil.LOGGER.warn("Unhandled code action: " + specialKey.action()); @@ -177,11 +199,8 @@ private void onPress() { } } - case KeyboardLayout.Key.ChangeLayoutKey changeLayoutKey -> { - ResourceLocation layoutId = changeLayoutKey.otherLayout(); - KeyboardLayoutWithId layoutWithId = Controlify.instance().keyboardLayoutManager().getLayout(layoutId); - this.keyboard.updateLayout(layoutWithId); - } + case KeyboardLayout.KeyFunction.ChangeLayoutFunc func -> + this.changeLayout(func.layout()); } if (!wasShiftAction && this.keyboard.isShifted() && !this.keyboard.isShiftLocked()) { @@ -193,11 +212,59 @@ private void onPress() { } public KeyboardLayout.Key getKey() { - return this.key.get(this.keyboard.isShifted()); + return this.key; + } + + public KeyboardLayout.KeyFunction getKeyFunction() { + return this.key.getFunction(this.keyboard.isShifted()); } public boolean isVisuallyPressed() { - return this.buttonPressed || this.shortcutPressed || this.mousePressed; + return this.buttonPressed + || this.shortcutPressed + || this.mousePressed + || this.isShiftKeyAndShifting() + || this.isShiftLockKeyAndShiftLocked(); + } + + private boolean isShiftKeyAndShifting() { + return this.keyboard.isShifted() && !this.keyboard.isShiftLocked() + && this.getKeyFunction() instanceof KeyboardLayout.KeyFunction.SpecialFunc(KeyboardLayout.KeyFunction.SpecialFunc.Action action) + && action == KeyboardLayout.KeyFunction.SpecialFunc.Action.SHIFT; + } + + private boolean isShiftLockKeyAndShiftLocked() { + return this.keyboard.isShiftLocked() + && this.getKeyFunction() instanceof KeyboardLayout.KeyFunction.SpecialFunc(KeyboardLayout.KeyFunction.SpecialFunc.Action action) + && action == KeyboardLayout.KeyFunction.SpecialFunc.Action.SHIFT_LOCK; + } + + private boolean supportsAction(boolean shifted) { + boolean supportsCharInput = this.keyboard.getInputTarget().supportsCharInput(); + boolean supportsKeyCodeInput = this.keyboard.getInputTarget().supportsKeyCodeInput(); + boolean supportsCopying = this.keyboard.getInputTarget().supportsCopying(); + + return switch (this.getKey().getFunction(shifted)) { + case KeyboardLayout.KeyFunction.StringFunc ignored -> supportsCharInput; + case KeyboardLayout.KeyFunction.CodeFunc ignored -> supportsKeyCodeInput; + case KeyboardLayout.KeyFunction.SpecialFunc specialFunc -> + switch (specialFunc.action()) { + case ENTER, BACKSPACE, LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW -> supportsKeyCodeInput; + case TAB, PASTE -> supportsCharInput; + case COPY_ALL -> supportsCopying; + case SHIFT, SHIFT_LOCK, PREVIOUS_LAYOUT -> true; + }; + case KeyboardLayout.KeyFunction.ChangeLayoutFunc ignored -> true; + }; + } + + private boolean supportsAction() { + return this.keyboard.isShifted() ? this.supportsShifted : this.supportsRegular; + } + + private void changeLayout(ResourceLocation layoutId) { + KeyboardLayoutWithId layoutWithId = Controlify.instance().keyboardLayoutManager().getLayout(layoutId); + this.keyboard.updateLayout(layoutWithId); } @Override @@ -205,7 +272,11 @@ protected void updateWidgetNarration(NarrationElementOutput narrationElementOutp } - private static void insertText(String text, KeyboardInputConsumer inputConsumer) { + private static void insertText(String text, InputTarget inputConsumer) { + // One `char` is not necessarily one visible character. + // Some characters, such as emojis, are represented using surrogate pairs, + // meaning they span two `char`s. + // Code points are used to represent characters that *may* be represented by surrogate pairs. text.codePoints().forEach((codePoint) -> { // guess the modifier based on the nature of the character int modCapital = Character.isUpperCase(codePoint) ? GLFW.GLFW_MOD_SHIFT : 0; @@ -220,15 +291,15 @@ private static void insertText(String text, KeyboardInputConsumer inputConsumer) }); } - private static Component createLabel(KeyboardLayout.ShiftableKey shiftableKey, boolean shift) { - KeyboardLayout.Key key = shiftableKey.get(shift); + private static Component createLabel(KeyboardLayout.Key key, boolean shift) { + KeyboardLayout.KeyFunction keyFunction = key.getFunction(shift); - return shiftableKey.shortcutBinding() + return key.shortcutBinding() .map(b -> BindingFontHelper.binding(b.bindId())) .map(glyph -> Component.empty() .append(glyph) .append(" ") - .append(key.displayName())) - .orElseGet(key::displayName); + .append(keyFunction.displayName())) + .orElseGet(keyFunction::displayName); } } diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardInputConsumer.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardInputConsumer.java deleted file mode 100644 index 44063b15..00000000 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardInputConsumer.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.isxander.controlify.screenkeyboard; - -public interface KeyboardInputConsumer { - void acceptChar(char ch, int modifiers); - - void acceptKeyCode(int keycode, int scancode, int modifiers); -} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java index 1574cf75..dde420df 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java @@ -21,10 +21,18 @@ import java.util.function.Function; import java.util.stream.Stream; -public record KeyboardLayout(float width, List> keys) { +/** + * Represents a keyboard layout. + * A keyboard layout consists of keys within rows, it is a column-aligned layout, + * meaning keys cannot span multiple columns. + * A keyboard layout has a single shift-layer, defined by the {@link Key}. + * @param width the unit width of the keyboard layout. all rows must sum to this width. + * @param keys the rows of keys in the keyboard layout, row-major order. + */ +public record KeyboardLayout(float width, List> keys) { public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.floatRange(1, Float.MAX_VALUE).fieldOf("width").forGetter(KeyboardLayout::width), - ShiftableKey.CODEC + Key.CODEC .listOf(1, Integer.MAX_VALUE) .listOf(1, Integer.MAX_VALUE) .fieldOf("keys").forGetter(KeyboardLayout::keys) @@ -38,114 +46,160 @@ public record KeyboardLayout(float width, List> keys) { public static boolean validateRowWidths(KeyboardLayout layout) { return layout.keys().stream() .mapToDouble(row -> row.stream() - .mapToDouble(ShiftableKey::width) + .mapToDouble(Key::width) .sum()) .allMatch(rowWidth -> rowWidth == layout.width()); } @SafeVarargs - public static KeyboardLayout of(float width, List... keys) { + public static KeyboardLayout of(float width, List... keys) { var layout = new KeyboardLayout(width, List.of(keys)); Validate.isTrue(validateRowWidths(layout), "All row widths do not match the specified row width: " + width); return layout; } - public record ShiftableKey(Key regular, Key shifted, float width, Optional shortcutBinding) { + /** + * Represents a key within a keyboard layout. + *

+ * All keys have a regular and a shifted function, a shifted function can be created + * from a regular function with {@link KeyFunction#createShifted()} if it is not explicitly defined. + * Regular functions can return themselves as shifted functions if they do not support shifting. + *

+ * Keys have a unit width, all keys within a row must sum to the width of the keyboard, defined by {@link KeyboardLayout#width()}. + * When rendered, the unit width of the key is multiplied by the real width of the keyboard to determine the actual pixel width of the key. + *

+ * Keys can also have an optional shortcut binding, which is used to display a shortcut for the key in the UI. + *

+ * Each key function provides the display name of the key with {@link KeyFunction#displayName()}. + * @param regular the regular key function, which is used when the shift is not enabled. + * @param shifted the shifted key function, which is used when the shift is enabled + * @param width the unit width of the key, which is multiplied by the keyboard width to determine the actual pixel width of the key + * @param shortcutBinding an optional shortcut binding for the key, used to display a shortcut in the UI + * @param identifier an optional identifier for the key, used when changing layouts to focus a specific key with a matching identifier + */ + public record Key(KeyFunction regular, KeyFunction shifted, float width, Optional shortcutBinding, @Nullable String identifier) { private static final Codec WIDTH_CODEC = Codec.floatRange(0.1f, Float.MAX_VALUE); - private static final Codec PAIR_CODEC = Codec.withAlternative( + private static final Codec PAIR_CODEC = Codec.withAlternative( RecordCodecBuilder.create(instance -> instance.group( - Key.CODEC.fieldOf("regular").forGetter(ShiftableKey::regular), - Key.CODEC.optionalFieldOf("shifted").forGetter(k -> Optional.of(k.shifted)), - WIDTH_CODEC.optionalFieldOf("width", 1f).forGetter(ShiftableKey::width), - InputBindingSupplier.CODEC.optionalFieldOf("shortcut").forGetter(ShiftableKey::shortcutBinding) - ).apply(instance, ShiftableKey::fromCodec)), - CExtraCodecs.arrayPair(Key.CODEC, ShiftableKey::regular, ShiftableKey::shifted, ShiftableKey::new) + KeyFunction.CODEC.fieldOf("regular").forGetter(Key::regular), + KeyFunction.CODEC.optionalFieldOf("shifted").forGetter(k -> Optional.of(k.shifted)), + WIDTH_CODEC.optionalFieldOf("width", 1f).forGetter(Key::width), + InputBindingSupplier.CODEC.optionalFieldOf("shortcut").forGetter(Key::shortcutBinding), + Codec.STRING.optionalFieldOf("identifier").forGetter(k -> Optional.ofNullable(k.identifier)) + ).apply(instance, Key::fromCodec)), + CExtraCodecs.arrayPair(KeyFunction.CODEC, Key::regular, Key::shifted, Key::new) ); - public static Codec CODEC = Codec.either(Key.CODEC, PAIR_CODEC) + public static Codec CODEC = Codec.either(KeyFunction.CODEC, PAIR_CODEC) .xmap( - either -> either.map(ShiftableKey::new, Function.identity()), + either -> either.map(Key::new, Function.identity()), Either::right ); - public ShiftableKey(Key key) { - this(key, key.createUppercase()); + public Key(KeyFunction keyFunction) { + this(keyFunction, keyFunction.createShifted()); } - public ShiftableKey(Key key, float width) { - this(key, width, Optional.empty()); + public Key(KeyFunction keyFunction, float width) { + this(keyFunction, width, Optional.empty()); } - public ShiftableKey(Key key, float width, Optional shortcut) { - this(key, key.createUppercase(), width, shortcut); + public Key(KeyFunction keyFunction, float width, Optional shortcut) { + this(keyFunction, keyFunction.createShifted(), width, shortcut, null); } - public ShiftableKey(Key regular, Key shifted) { - this(regular, shifted, 1f, Optional.empty()); + public Key(KeyFunction regular, KeyFunction shifted) { + this(regular, shifted, 1f, Optional.empty(), null); } - private static ShiftableKey fromCodec(Key regular, Optional shifted, float width, Optional shortcut) { - return new ShiftableKey( + private static Key fromCodec(KeyFunction regular, Optional shifted, float width, Optional shortcut, Optional identifier) { + return new Key( regular, - shifted.orElseGet(regular::createUppercase), + shifted.orElseGet(regular::createShifted), width, - shortcut + shortcut, + identifier.orElse(null) ); } - public Key get(boolean shifted) { + public KeyFunction getFunction(boolean shifted) { return shifted ? this.shifted : this.regular; } } - public sealed interface Key { + /** + * Represents a function of a key, which could be inserting a string, + * imitating a key code, performing a special action, or changing the keyboard layout. + *

+ * Functions are responsible for defining what happens when a key is pressed, + * as well as proving a display name for the key. + *

+ * The implementation of the function is not defined here, and is up to the consumer, + * in this case, {@link KeyWidget}, to handle each sealed implementation of this interface. + */ + public sealed interface KeyFunction { + /** + * Returns the display name of the key function. + * Used to display the key in the UI. + * @return the display name of the key function + */ Component displayName(); - @Nullable String identifier(); - - default Key createUppercase() { + /** + * Creates a shifted version of this key function. + * May return itself if the function does not support shifting. + * @return the shifted key function + */ + default KeyFunction createShifted() { return this; } - Codec CODEC = new Codec<>() { + Codec CODEC = new Codec<>() { @Override - public DataResult encode(Key input, DynamicOps ops, T prefix) { + public DataResult encode(KeyFunction input, DynamicOps ops, T prefix) { return switch (input) { - case StringKey stringKey -> StringKey.CODEC.encode(stringKey, ops, prefix); - case CodeKey codeKey -> CodeKey.CODEC.encode(codeKey, ops, prefix); - case SpecialKey specialKey -> SpecialKey.CODEC.encode(specialKey, ops, prefix); - case ChangeLayoutKey changeLayoutKey -> ChangeLayoutKey.CODEC.encode(changeLayoutKey, ops, prefix); + case StringFunc stringKey -> StringFunc.CODEC.encode(stringKey, ops, prefix); + case CodeFunc codeKey -> CodeFunc.CODEC.encode(codeKey, ops, prefix); + case SpecialFunc specialKey -> SpecialFunc.CODEC.encode(specialKey, ops, prefix); + case ChangeLayoutFunc changeLayoutKey -> ChangeLayoutFunc.CODEC.encode(changeLayoutKey, ops, prefix); }; } @Override - public DataResult> decode(DynamicOps ops, T input) { - return Stream.of(StringKey.CODEC, CodeKey.CODEC, SpecialKey.CODEC, ChangeLayoutKey.CODEC) + public DataResult> decode(DynamicOps ops, T input) { + return Stream.of(StringFunc.CODEC, CodeFunc.CODEC, SpecialFunc.CODEC, ChangeLayoutFunc.CODEC) .map(decoder -> decoder.decode(ops, input)) - .map(r -> r.map(p -> p.mapFirst(t -> (Key) t))) + .map(r -> r.map(p -> p.mapFirst(t -> (KeyFunction) t))) .filter(DataResult::isSuccess) .findFirst() .orElseGet(() -> DataResult.error(() -> "No decoder matched.")); } }; - record StringKey(String string, @Nullable Component manualDisplayName, @Nullable String identifier) implements Key { - public static final Codec CODEC = Codec.withAlternative( + /** + * A key function that inserts a string when pressed. + *

+ * This function supports {@link #createShifted()} which uses {@link String#toUpperCase()} to create a shifted version of the string. + * + * @param string the string to insert when the key is pressed + * @param manualDisplayName an optional manual display name for the key, if not provided, the string itself is used when returning {@link #displayName()} + */ + record StringFunc(String string, @Nullable Component manualDisplayName) implements KeyFunction { + public static final Codec CODEC = Codec.withAlternative( RecordCodecBuilder.create(instance -> instance.group( - Codec.STRING.fieldOf("chars").forGetter(StringKey::string), - ComponentSerialization.CODEC.optionalFieldOf("display_name").forGetter(k -> Optional.of(k.displayName())), - Codec.STRING.optionalFieldOf("identifier").forGetter(k -> Optional.ofNullable(k.identifier)) - ).apply(instance, StringKey::fromCodec)), - Codec.STRING.xmap(StringKey::new, StringKey::string) + Codec.STRING.fieldOf("chars").forGetter(StringFunc::string), + ComponentSerialization.CODEC.optionalFieldOf("display_name").forGetter(k -> Optional.of(k.displayName())) + ).apply(instance, StringFunc::fromCodec)), + Codec.STRING.xmap(StringFunc::new, StringFunc::string) ); - public StringKey(String string) { - this(string, null, null); + public StringFunc(String string) { + this(string, null); } - private static StringKey fromCodec(String string, Optional displayName, Optional identifier) { - return new StringKey(string, displayName.orElse(null), identifier.orElse(null)); + private static StringFunc fromCodec(String string, Optional displayName) { + return new StringFunc(string, displayName.orElse(null)); } @Override @@ -154,42 +208,62 @@ public Component displayName() { } @Override - public Key createUppercase() { - return new StringKey(string.toUpperCase(), manualDisplayName, identifier); + public KeyFunction createShifted() { + return new StringFunc(string.toUpperCase(), manualDisplayName); } } - record CodeKey(int keycode, int scancode, int modifier, Component displayName, @Nullable String identifier) implements Key { - public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( - Codec.INT.fieldOf("keycode").forGetter(CodeKey::keycode), - Codec.INT.optionalFieldOf("scancode", 0).forGetter(CodeKey::scancode), - Codec.INT.optionalFieldOf("modifier", 0).forGetter(CodeKey::modifier), - ComponentSerialization.CODEC.fieldOf("display_name").forGetter(CodeKey::displayName), - Codec.STRING.optionalFieldOf("identifier").forGetter(k -> Optional.ofNullable(k.identifier)) - ).apply(instance, CodeKey::fromCodec)); - - public CodeKey(int keycode, int scancode, int modifier, Component displayName) { - this(keycode, scancode, modifier, displayName, null); - } + /** + * A key function that inserts a list of key codes when pressed. + *

+ * This function does not support {@link #createShifted()} as key codes cannot be automatically upper-cased. + * + * @param codes the list of key codes to insert when the key is pressed + * @param displayName the display name of the key function, used to display the key in the UI + */ + record CodeFunc(List codes, Component displayName) implements KeyFunction { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + KeyCode.CODEC.listOf().fieldOf("codes").forGetter(CodeFunc::codes), + ComponentSerialization.CODEC.fieldOf("display_name").forGetter(CodeFunc::displayName) + ).apply(instance, CodeFunc::new)); + + /** + * A key code is a combination of a keycode, scancode, and modifier. + * @param keycode is the logical key code, platform agnostic. use {@link com.mojang.blaze3d.platform.InputConstants} to get the key code. + * @param scancode is the physical key code, platform specific. usually leaving blank is fine since no one ever looks at it. + * @param modifier is the modifier bitset, which can be used to specify additional key modifiers like shift, ctrl, alt, etc. + * use {@link org.lwjgl.glfw.GLFW#GLFW_MOD_SHIFT} etc + */ + record KeyCode(int keycode, int scancode, int modifier) { + public static final Codec CODEC = Codec.withAlternative( + RecordCodecBuilder.create(instance -> instance.group( + Codec.INT.fieldOf("keycode").forGetter(KeyCode::keycode), + Codec.INT.optionalFieldOf("scancode", 0).forGetter(KeyCode::scancode), + Codec.INT.optionalFieldOf("modifier", 0).forGetter(KeyCode::modifier) + ).apply(instance, KeyCode::new)), + Codec.INT.xmap(keycode -> new KeyCode(keycode, 0, 0), KeyCode::keycode) + ); - private static CodeKey fromCodec(int keycode, int scancode, int modifier, Component displayName, Optional identifier) { - return new CodeKey(keycode, scancode, modifier, displayName, identifier.orElse(null)); + public KeyCode(int keycode) { + this(keycode, 0, 0); + } } } - record SpecialKey(Action action, @Nullable String identifier) implements Key { - public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( - StringRepresentable.fromEnum(Action::values).fieldOf("action").forGetter(SpecialKey::action), - Codec.STRING.optionalFieldOf("identifier").forGetter(k -> Optional.ofNullable(k.identifier)) - ).apply(instance, SpecialKey::fromCodec)); - - public SpecialKey(Action action) { - this(action, null); - } - - private static SpecialKey fromCodec(Action action, Optional identifier) { - return new SpecialKey(action, identifier.orElse(null)); - } + /** + * A key function that performs a special action when pressed. + *

+ * Some of these special actions may be shorthands for other key functions, + * such as inserting specific key codes like {@link Action#LEFT_ARROW}. + *

+ * This function does not support {@link #createShifted()} as special actions cannot be automatically upper-cased. + * + * @param action the action to perform when the key is pressed + */ + record SpecialFunc(Action action) implements KeyFunction { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + StringRepresentable.fromEnum(Action::values).fieldOf("action").forGetter(SpecialFunc::action) + ).apply(instance, SpecialFunc::new)); @Override public Component displayName() { @@ -227,15 +301,27 @@ public Component displayName() { } } - record ChangeLayoutKey(ResourceLocation otherLayout, Component displayName, @Nullable String identifier) implements Key { - public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( - ResourceLocation.CODEC.fieldOf("layout").forGetter(ChangeLayoutKey::otherLayout), - ComponentSerialization.CODEC.fieldOf("display_name").forGetter(ChangeLayoutKey::displayName), - Codec.STRING.optionalFieldOf("identifier").forGetter(k -> Optional.ofNullable(k.identifier)) - ).apply(instance, ChangeLayoutKey::fromCodec)); - - private static ChangeLayoutKey fromCodec(ResourceLocation layout, Component displayName, Optional identifier) { - return new ChangeLayoutKey(layout, displayName, identifier.orElse(null)); + /** + * A key function that changes the keyboard layout when pressed. + * It can be any layout found by the resource reloader. A resource pack + * could make their own arbitrarily named layout and reference it here. + *

+ * If the layout referenced is not found, the {@link FallbackKeyboardLayout fallback layout} will be used instead. + *

+ * This function does not support {@link #createShifted()} as layouts cannot be automatically upper-cased. + * + * @param layout the layout id to switch to when the key is pressed. + * @param displayName the display name of the key function, used to display the key in the UI + */ + record ChangeLayoutFunc(ResourceLocation layout, Component displayName) implements KeyFunction { + + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + ResourceLocation.CODEC.fieldOf("layout").forGetter(ChangeLayoutFunc::layout), + ComponentSerialization.CODEC.fieldOf("display_name").forGetter(ChangeLayoutFunc::displayName) + ).apply(instance, ChangeLayoutFunc::fromCodec)); + + private static ChangeLayoutFunc fromCodec(ResourceLocation layout, Component displayName) { + return new ChangeLayoutFunc(layout, displayName); } } } diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java index 27693442..af95bd5c 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java @@ -1,6 +1,10 @@ package dev.isxander.controlify.screenkeyboard; import com.google.common.collect.ImmutableList; +import dev.isxander.controlify.bindings.ControlifyBindings; +import dev.isxander.controlify.controller.ControllerEntity; +import dev.isxander.controlify.screenop.ComponentProcessor; +import dev.isxander.controlify.screenop.ScreenProcessor; import dev.isxander.controlify.screenop.ScreenProcessorProvider; import dev.isxander.controlify.utils.render.Blit; import net.minecraft.client.gui.ComponentPath; @@ -19,9 +23,11 @@ import java.util.*; import java.util.function.Predicate; -public class KeyboardWidget extends AbstractWidget implements ContainerEventHandler { - private final ResourceLocation currentLayout; - private KeyboardInputConsumer inputConsumer; +public class KeyboardWidget extends AbstractWidget implements ContainerEventHandler, ComponentProcessor { + private ResourceLocation currentLayout; + private @Nullable ResourceLocation previousLayout; + + private InputTarget inputConsumer; private List keys = ImmutableList.of(); @@ -32,12 +38,11 @@ public class KeyboardWidget extends AbstractWidget implements ContainerEventHand private final Screen containingScreen; - public KeyboardWidget(int x, int y, int width, int height, KeyboardLayoutWithId layout, KeyboardInputConsumer inputConsumer, Screen containingScreen) { + public KeyboardWidget(int x, int y, int width, int height, KeyboardLayoutWithId layout, InputTarget inputConsumer, Screen containingScreen) { super(x, y, width, height, Component.literal("On-Screen Keyboard")); this.inputConsumer = inputConsumer; this.containingScreen = containingScreen; - this.currentLayout = layout.id(); - this.updateLayout(layout.layout(), "initial_focus", null); + this.updateLayout(layout, "initial_focus", null); } public void updateLayout(KeyboardLayoutWithId layout) { @@ -46,18 +51,21 @@ public void updateLayout(KeyboardLayoutWithId layout) { .map(k -> k.getKey().identifier()) .orElse(null); - this.updateLayout(layout.layout(), oldIdentifier, oldLayoutId); + this.updateLayout(layout, oldIdentifier, oldLayoutId); } - public void updateLayout(KeyboardLayout layout, @Nullable String identifierToFocus, @Nullable ResourceLocation oldLayoutChangerToFocus) { - this.arrangeKeys(layout); + public void updateLayout(KeyboardLayoutWithId layout, @Nullable String identifierToFocus, @Nullable ResourceLocation oldLayoutChangerToFocus) { + this.previousLayout = this.currentLayout; + this.currentLayout = layout.id(); + + this.arrangeKeys(layout.layout()); findKey( identifierToFocus != null, k -> Objects.equals(k.getKey().identifier(), identifierToFocus) ).or(() -> findKey( oldLayoutChangerToFocus != null, - k -> k.getKey() instanceof KeyboardLayout.Key.ChangeLayoutKey changeLayoutKey && changeLayoutKey.otherLayout().equals(oldLayoutChangerToFocus)) + k -> k.getKeyFunction() instanceof KeyboardLayout.KeyFunction.ChangeLayoutFunc changeLayoutKey && (changeLayoutKey.layout().equals(oldLayoutChangerToFocus) || changeLayoutKey.isPreviousLayout())) ); } @@ -71,9 +79,9 @@ private void arrangeKeys(KeyboardLayout layout) { float keyHeight = (float) this.getHeight() / layout.keys().size(); float y = this.getY(); - for (List row : layout.keys()) { + for (List row : layout.keys()) { float x = this.getX(); - for (KeyboardLayout.ShiftableKey key : row) { + for (KeyboardLayout.Key key : row) { float keyWidth = key.width() * unitWidth; var keyWidget = new KeyWidget( @@ -117,6 +125,12 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo }); } + @Override + public boolean overrideControllerButtons(ScreenProcessor screen, ControllerEntity controller) { + // prevent default button handling for gui press which would send enter which is most likely submit + return ControlifyBindings.GUI_PRESS.on(controller).guiPressed().get(); + } + public void setShifted(boolean shifted) { this.shifted = shifted; } @@ -133,11 +147,11 @@ public boolean isShiftLocked() { return shiftLocked; } - public void setInputConsumer(KeyboardInputConsumer inputConsumer) { + public void setInputTarget(InputTarget inputConsumer) { this.inputConsumer = inputConsumer; } - public KeyboardInputConsumer getInputConsumer() { + public InputTarget getInputTarget() { return inputConsumer; } @@ -145,6 +159,10 @@ public ResourceLocation getCurrentLayoutId() { return this.currentLayout; } + public Optional getPreviousLayoutId() { + return Optional.ofNullable(this.previousLayout); + } + private Optional findKey(boolean skipSearch, Predicate predicate) { if (skipSearch) { return Optional.empty(); @@ -182,8 +200,17 @@ public void setDragging(boolean dragging) { @Override public void setFocused(@Nullable GuiEventListener focused) { - if (focused != null && (!(focused instanceof KeyWidget) || !this.keys.contains(focused))) { - throw new IllegalArgumentException("Focused widget must be a KeyWidget in this KeyboardWidget"); + if (focused != null) { + if (!(focused instanceof KeyWidget)) { + throw new IllegalArgumentException("Focused widget must be a KeyWidget in this KeyboardWidget"); + } + + // This case happens when mouse clicking on a change_layout key + // since the action happens first which removes the key from the list, + // and then the container sets the focus, which is no longer in the key list. + if (!this.keys.contains(focused)) { + focused = null; + } } if (this.focused != null) { diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/MixinInputTarget.java b/src/main/java/dev/isxander/controlify/screenkeyboard/MixinInputTarget.java new file mode 100644 index 00000000..186ec844 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/MixinInputTarget.java @@ -0,0 +1,45 @@ +package dev.isxander.controlify.screenkeyboard; + +public interface MixinInputTarget extends InputTarget { + default boolean controlify$supportsCharInput() { + return false; + } + default boolean controlify$acceptChar(char ch, int modifiers) { + return false; + } + + default boolean controlify$supportsKeyCodeInput() { + return false; + } + default boolean controlify$acceptKeyCode(int keycode, int scancode, int modifiers) { + return false; + } + + default boolean controlify$supportsCopying() { + return false; + } + default boolean controlify$copy() { + return false; + } + + default boolean supportsCharInput() { + return controlify$supportsCharInput(); + } + default boolean acceptChar(char ch, int modifiers) { + return controlify$acceptChar(ch, modifiers); + } + + default boolean supportsKeyCodeInput() { + return controlify$supportsKeyCodeInput(); + } + default boolean acceptKeyCode(int keycode, int scancode, int modifiers) { + return controlify$acceptKeyCode(keycode, scancode, modifiers); + } + + default boolean supportsCopying() { + return controlify$supportsCopying(); + } + default boolean copy() { + return controlify$copy(); + } +} diff --git a/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java b/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java index be47825b..51425650 100644 --- a/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java @@ -34,7 +34,7 @@ public class ScreenProcessor { public final T screen; - protected final HoldRepeatHelper holdRepeatHelper = new HoldRepeatHelper(10, 3); + protected final HoldRepeatHelper holdRepeatHelper; protected static final Minecraft minecraft = Minecraft.getInstance(); private final List eventListeners = new ArrayList<>(); @@ -44,6 +44,7 @@ public ScreenProcessor(T screen) { if (screen instanceof ScreenControllerEventListener eventListener) { eventListeners.add(eventListener); } + this.holdRepeatHelper = createHoldRepeatHelper(); } public void onControllerUpdate(ControllerEntity controller) { @@ -274,6 +275,10 @@ public void addEventListener(ScreenControllerEventListener listener) { eventListeners.add(listener); } + protected HoldRepeatHelper createHoldRepeatHelper() { + return new HoldRepeatHelper(10, 3); + } + protected Queue getFocusTree() { if (screen.getFocused() == null) return new ArrayDeque<>(); diff --git a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/ChatScreenProcessor.java b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/ChatScreenProcessor.java new file mode 100644 index 00000000..709c7d57 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/ChatScreenProcessor.java @@ -0,0 +1,16 @@ +package dev.isxander.controlify.screenop.compat.vanilla; + +import dev.isxander.controlify.screenop.ScreenProcessor; +import dev.isxander.controlify.utils.HoldRepeatHelper; +import net.minecraft.client.gui.screens.ChatScreen; + +public class ChatScreenProcessor extends ScreenProcessor { + public ChatScreenProcessor(ChatScreen screen) { + super(screen); + } + + @Override + protected HoldRepeatHelper createHoldRepeatHelper() { + return new HoldRepeatHelper(3, 4); + } +} From a360d8c67f742b9b3e6d8c499b3055d51fa04715 Mon Sep 17 00:00:00 2001 From: isxander Date: Thu, 7 Aug 2025 21:54:36 +0100 Subject: [PATCH 04/12] finishing touches, polish chat menu experience, add hints, remove native keyboard capabilities as it was not used, create a "secondary GUI navigation" with Right Stick, replace option cycle binds with left/right secondary navigation --- .../bindings/ControlifyBindings.java | 42 +++-- .../screenop/CycleControlProcessor.java | 4 +- .../screenop/TickBoxControlProcessor.java | 2 +- ...ngControllerElementComponentProcessor.java | 4 +- ...erControllerElementComponentProcessor.java | 8 +- .../controller/ControllerEntity.java | 6 - .../controller/GenericControllerConfig.java | 3 + .../keyboard/NativeKeyboardComponent.java | 45 ----- .../driver/steamdeck/SteamDeckDriver.java | 10 -- .../controlify/font/BindingFontHelper.java | 5 + .../screen/ControllerConfigScreenFactory.java | 69 +------- .../screenkeyboard/ChatScreenMixin.java | 94 ++++++---- .../CommandSuggestionsMixin.java | 29 +++- .../screenkeyboard/KeyboardLayout.java | 2 + .../screenkeyboard/KeyboardWidget.java | 8 +- .../AbstractSliderComponentProcessor.java | 8 +- .../BundleItemSlotControllerAction.java | 9 +- .../compat/vanilla/ChatScreenProcessor.java | 160 +++++++++++++++++- .../controlify/utils/LazyComponentDims.java | 40 +++++ .../controllers/default_bind/default.json | 12 +- .../assets/controlify/lang/en_us.json | 11 +- 21 files changed, 363 insertions(+), 208 deletions(-) delete mode 100644 src/main/java/dev/isxander/controlify/controller/keyboard/NativeKeyboardComponent.java create mode 100644 src/main/java/dev/isxander/controlify/utils/LazyComponentDims.java diff --git a/src/main/java/dev/isxander/controlify/bindings/ControlifyBindings.java b/src/main/java/dev/isxander/controlify/bindings/ControlifyBindings.java index 4e94fd78..b5c684f9 100644 --- a/src/main/java/dev/isxander/controlify/bindings/ControlifyBindings.java +++ b/src/main/java/dev/isxander/controlify/bindings/ControlifyBindings.java @@ -159,23 +159,7 @@ public final class ControlifyBindings { public static final InputBindingSupplier DROP_INVENTORY = ControlifyBindApi.get().registerBinding(builder -> builder .id("controlify", "drop_inventory") .category(INVENTORY_CATEGORY) - .allowedContexts(BindContext.CONTAINER)); - public static final InputBindingSupplier BUNDLE_NAVI_UP = ControlifyBindApi.get().registerBinding(builder -> builder - .id("controlify", "bundle_navi_up") - .category(INVENTORY_CATEGORY) - .allowedContexts(BindContext.CONTAINER)); - public static final InputBindingSupplier BUNDLE_NAVI_DOWN = ControlifyBindApi.get().registerBinding(builder -> builder - .id("controlify", "bundle_navi_down") - .category(INVENTORY_CATEGORY) - .allowedContexts(BindContext.CONTAINER)); - public static final InputBindingSupplier BUNDLE_NAVI_LEFT = ControlifyBindApi.get().registerBinding(builder -> builder - .id("controlify", "bundle_navi_left") - .category(INVENTORY_CATEGORY) - .allowedContexts(BindContext.CONTAINER)); - public static final InputBindingSupplier BUNDLE_NAVI_RIGHT = ControlifyBindApi.get().registerBinding(builder -> builder - .id("controlify", "bundle_navi_right") - .category(INVENTORY_CATEGORY) - .allowedContexts(BindContext.CONTAINER)); + .allowedContexts(BindContext.CONTAINER, BindContext.REGULAR_SCREEN)); public static final InputBindingSupplier PICK_BLOCK = ControlifyBindApi.get().registerBinding(builder -> builder .id("controlify", "pick_block") @@ -286,14 +270,26 @@ public final class ControlifyBindings { .id("controlify", "gui_navi_right") .category(GUI_CATEGORY) .allowedContexts(BindContext.REGULAR_SCREEN)); - public static final InputBindingSupplier CYCLE_OPT_FORWARD = ControlifyBindApi.get().registerBinding(builder -> builder - .id("controlify", "cycle_opt_forward") + public static final InputBindingSupplier GUI_SECONDARY_NAVI_UP = ControlifyBindApi.get().registerBinding(builder -> builder + .id("controlify", "gui_secondary_navi_up") .category(GUI_CATEGORY) - .allowedContexts(BindContext.REGULAR_SCREEN)); - public static final InputBindingSupplier CYCLE_OPT_BACKWARD = ControlifyBindApi.get().registerBinding(builder -> builder - .id("controlify", "cycle_opt_backward") + .allowedContexts(BindContext.CONTAINER, BindContext.REGULAR_SCREEN)); + public static final InputBindingSupplier GUI_SECONDARY_NAVI_DOWN = ControlifyBindApi.get().registerBinding(builder -> builder + .id("controlify", "gui_secondary_navi_down") .category(GUI_CATEGORY) - .allowedContexts(BindContext.REGULAR_SCREEN)); + .allowedContexts(BindContext.CONTAINER, BindContext.REGULAR_SCREEN)); + public static final InputBindingSupplier GUI_SECONDARY_NAVI_LEFT = ControlifyBindApi.get().registerBinding(builder -> builder + .id("controlify", "gui_secondary_navi_left") + .category(GUI_CATEGORY) + .allowedContexts(BindContext.CONTAINER, BindContext.REGULAR_SCREEN)); + public static final InputBindingSupplier GUI_SECONDARY_NAVI_RIGHT = ControlifyBindApi.get().registerBinding(builder -> builder + .id("controlify", "gui_secondary_navi_right") + .category(GUI_CATEGORY) + .allowedContexts(BindContext.CONTAINER, BindContext.REGULAR_SCREEN)); + @Deprecated + public static final InputBindingSupplier CYCLE_OPT_FORWARD = GUI_SECONDARY_NAVI_RIGHT; + @Deprecated + public static final InputBindingSupplier CYCLE_OPT_BACKWARD = GUI_SECONDARY_NAVI_LEFT; public static final InputBindingSupplier RADIAL_MENU = ControlifyBindApi.get().registerBinding(builder -> builder .id("controlify", "radial_menu") diff --git a/src/main/java/dev/isxander/controlify/compatibility/sodium/screenop/CycleControlProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/sodium/screenop/CycleControlProcessor.java index f6df27c2..372722cc 100644 --- a/src/main/java/dev/isxander/controlify/compatibility/sodium/screenop/CycleControlProcessor.java +++ b/src/main/java/dev/isxander/controlify/compatibility/sodium/screenop/CycleControlProcessor.java @@ -16,13 +16,13 @@ public CycleControlProcessor(Consumer cycleMethod) { @Override public boolean overrideControllerButtons(ScreenProcessor screen, ControllerEntity controller) { - if (ControlifyBindings.CYCLE_OPT_FORWARD.on(controller).justPressed() + if (ControlifyBindings.GUI_SECONDARY_NAVI_RIGHT.on(controller).justPressed() || ControlifyBindings.GUI_PRESS.on(controller).justPressed() ) { cycleMethod.accept(false); return true; } - if (ControlifyBindings.CYCLE_OPT_BACKWARD.on(controller).justPressed()) { + if (ControlifyBindings.GUI_SECONDARY_NAVI_LEFT.on(controller).justPressed()) { cycleMethod.accept(true); return true; } diff --git a/src/main/java/dev/isxander/controlify/compatibility/sodium/screenop/TickBoxControlProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/sodium/screenop/TickBoxControlProcessor.java index 3d68cbaa..1c5393f9 100644 --- a/src/main/java/dev/isxander/controlify/compatibility/sodium/screenop/TickBoxControlProcessor.java +++ b/src/main/java/dev/isxander/controlify/compatibility/sodium/screenop/TickBoxControlProcessor.java @@ -18,7 +18,7 @@ public boolean overrideControllerButtons(ScreenProcessor screen, ControllerEn toggleMethod.run(); return true; } - if (ControlifyBindings.CYCLE_OPT_FORWARD.on(controller).justPressed() || ControlifyBindings.CYCLE_OPT_BACKWARD.on(controller).justPressed()) { + if (ControlifyBindings.GUI_SECONDARY_NAVI_RIGHT.on(controller).justPressed() || ControlifyBindings.GUI_SECONDARY_NAVI_LEFT.on(controller).justPressed()) { toggleMethod.run(); return true; } diff --git a/src/main/java/dev/isxander/controlify/compatibility/yacl/screenop/CyclingControllerElementComponentProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/yacl/screenop/CyclingControllerElementComponentProcessor.java index a44105a9..4bebcd79 100644 --- a/src/main/java/dev/isxander/controlify/compatibility/yacl/screenop/CyclingControllerElementComponentProcessor.java +++ b/src/main/java/dev/isxander/controlify/compatibility/yacl/screenop/CyclingControllerElementComponentProcessor.java @@ -18,8 +18,8 @@ public CyclingControllerElementComponentProcessor(CyclingControllerElement cycli @Override public boolean overrideControllerNavigation(ScreenProcessor screen, ControllerEntity controller) { - boolean left = ControlifyBindings.CYCLE_OPT_BACKWARD.on(controller).digitalNow(); - boolean right = ControlifyBindings.CYCLE_OPT_FORWARD.on(controller).digitalNow(); + boolean left = ControlifyBindings.GUI_SECONDARY_NAVI_LEFT.on(controller).digitalNow(); + boolean right = ControlifyBindings.GUI_SECONDARY_NAVI_RIGHT.on(controller).digitalNow(); if (!((ControllerWidgetAccessor) cyclingController).getControl().option().available()) { return false; diff --git a/src/main/java/dev/isxander/controlify/compatibility/yacl/screenop/SliderControllerElementComponentProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/yacl/screenop/SliderControllerElementComponentProcessor.java index 81c2fc4c..ab74eec3 100644 --- a/src/main/java/dev/isxander/controlify/compatibility/yacl/screenop/SliderControllerElementComponentProcessor.java +++ b/src/main/java/dev/isxander/controlify/compatibility/yacl/screenop/SliderControllerElementComponentProcessor.java @@ -18,10 +18,10 @@ public SliderControllerElementComponentProcessor(SliderControllerElement element @Override public boolean overrideControllerButtons(ScreenProcessor screen, ControllerEntity controller) { - var left = ControlifyBindings.CYCLE_OPT_BACKWARD.on(controller).digitalNow(); - var leftPrev = ControlifyBindings.CYCLE_OPT_BACKWARD.on(controller).digitalPrev(); - var right = ControlifyBindings.CYCLE_OPT_FORWARD.on(controller).digitalNow(); - var rightPrev = ControlifyBindings.CYCLE_OPT_FORWARD.on(controller).digitalPrev(); + var left = ControlifyBindings.GUI_SECONDARY_NAVI_LEFT.on(controller).digitalNow(); + var leftPrev = ControlifyBindings.GUI_SECONDARY_NAVI_LEFT.on(controller).digitalPrev(); + var right = ControlifyBindings.GUI_SECONDARY_NAVI_RIGHT.on(controller).digitalNow(); + var rightPrev = ControlifyBindings.GUI_SECONDARY_NAVI_RIGHT.on(controller).digitalPrev(); if (!((ControllerWidgetAccessor) slider).getControl().option().available()) { return false; diff --git a/src/main/java/dev/isxander/controlify/controller/ControllerEntity.java b/src/main/java/dev/isxander/controlify/controller/ControllerEntity.java index 5b66b3d1..23b50d51 100644 --- a/src/main/java/dev/isxander/controlify/controller/ControllerEntity.java +++ b/src/main/java/dev/isxander/controlify/controller/ControllerEntity.java @@ -11,7 +11,6 @@ import dev.isxander.controlify.controller.info.DriverNameComponent; import dev.isxander.controlify.controller.info.GUIDComponent; import dev.isxander.controlify.controller.info.UIDComponent; -import dev.isxander.controlify.controller.keyboard.NativeKeyboardComponent; import dev.isxander.controlify.controller.led.LEDComponent; import dev.isxander.controlify.controller.serialization.ConfigHolder; import dev.isxander.controlify.controller.serialization.IConfig; @@ -159,11 +158,6 @@ public Optional bluetooth() { return this.getComponent(BluetoothDeviceComponent.ID); } - @Contract(pure = true) - public Optional nativeKeyboard() { - return this.getComponent(NativeKeyboardComponent.ID); - } - public void update(boolean outOfFocus) { this.driver.update(this, outOfFocus); } diff --git a/src/main/java/dev/isxander/controlify/controller/GenericControllerConfig.java b/src/main/java/dev/isxander/controlify/controller/GenericControllerConfig.java index ed9e0969..e4f26d58 100644 --- a/src/main/java/dev/isxander/controlify/controller/GenericControllerConfig.java +++ b/src/main/java/dev/isxander/controlify/controller/GenericControllerConfig.java @@ -25,4 +25,7 @@ public class GenericControllerConfig implements ConfigClass { public boolean showOnScreenKeyboard = true; public boolean dontShowControllerSubmission = false; + + public boolean hintChatCursorMovement = true; + public boolean hintCommandSuggester = true; } diff --git a/src/main/java/dev/isxander/controlify/controller/keyboard/NativeKeyboardComponent.java b/src/main/java/dev/isxander/controlify/controller/keyboard/NativeKeyboardComponent.java deleted file mode 100644 index 1cbb4f27..00000000 --- a/src/main/java/dev/isxander/controlify/controller/keyboard/NativeKeyboardComponent.java +++ /dev/null @@ -1,45 +0,0 @@ -package dev.isxander.controlify.controller.keyboard; - -import dev.isxander.controlify.controller.ECSComponent; -import dev.isxander.controlify.controller.impl.ConfigImpl; -import dev.isxander.controlify.controller.serialization.ConfigClass; -import dev.isxander.controlify.controller.serialization.ConfigHolder; -import dev.isxander.controlify.controller.serialization.IConfig; -import dev.isxander.controlify.utils.CUtil; -import net.minecraft.resources.ResourceLocation; - -public class NativeKeyboardComponent implements ECSComponent, ConfigHolder { - public static final ResourceLocation ID = CUtil.rl("native_keyboard"); - - private final ConfigImpl config = new ConfigImpl<>(Config::new, Config.class); - - private final Runnable onOpen; - private final float keyboardHeight; - - public NativeKeyboardComponent(Runnable onOpen, float keyboardHeight) { - this.onOpen = onOpen; - this.keyboardHeight = keyboardHeight; - } - - public void open() { - this.onOpen.run(); - } - - public float getKeyboardHeight() { - return this.keyboardHeight; - } - - @Override - public IConfig config() { - return config; - } - - @Override - public ResourceLocation id() { - return ID; - } - - public static class Config implements ConfigClass { - public boolean useNativeKeyboard = false; - } -} diff --git a/src/main/java/dev/isxander/controlify/driver/steamdeck/SteamDeckDriver.java b/src/main/java/dev/isxander/controlify/driver/steamdeck/SteamDeckDriver.java index 15539509..7ceb5328 100644 --- a/src/main/java/dev/isxander/controlify/driver/steamdeck/SteamDeckDriver.java +++ b/src/main/java/dev/isxander/controlify/driver/steamdeck/SteamDeckDriver.java @@ -12,7 +12,6 @@ import dev.isxander.controlify.controller.info.UIDComponent; import dev.isxander.controlify.controller.input.GamepadInputs; import dev.isxander.controlify.controller.input.InputComponent; -import dev.isxander.controlify.controller.keyboard.NativeKeyboardComponent; import dev.isxander.controlify.controller.steamdeck.SteamDeckComponent; import dev.isxander.controlify.controller.touchpad.TouchpadComponent; import dev.isxander.controlify.controller.touchpad.Touchpads; @@ -42,7 +41,6 @@ public class SteamDeckDriver implements Driver { private GyroComponent gyroComponent; private BatteryLevelComponent batteryLevelComponent; private TouchpadComponent touchpadComponent; - private NativeKeyboardComponent keyboardComponent; private ControlifyLogger logger; @@ -88,10 +86,6 @@ public void addComponents(ControllerEntity controller) { ) // there are two touchpads, one for each thumb ); - controller.setComponent( - this.keyboardComponent = new NativeKeyboardComponent(this::openKeyboard, 0.5f) - ); - controller.setComponent(new SteamDeckComponent()); } @@ -217,10 +211,6 @@ private void updateTouchpad(Touchpads.Touchpad touchpad, short x, short y, short } } - private void openKeyboard() { - deck.openModalKeyboard(true); - } - @Override public void close() { logger.debugLog("Closing driver..."); diff --git a/src/main/java/dev/isxander/controlify/font/BindingFontHelper.java b/src/main/java/dev/isxander/controlify/font/BindingFontHelper.java index 37ea0712..1ceca51b 100644 --- a/src/main/java/dev/isxander/controlify/font/BindingFontHelper.java +++ b/src/main/java/dev/isxander/controlify/font/BindingFontHelper.java @@ -3,6 +3,7 @@ import com.mojang.blaze3d.font.GlyphInfo; import com.mojang.blaze3d.font.SheetGlyphInfo; import dev.isxander.controlify.api.bind.InputBinding; +import dev.isxander.controlify.api.bind.InputBindingSupplier; import dev.isxander.controlify.mixins.feature.font.FontAccessor; import dev.isxander.controlify.utils.CUtil; import net.minecraft.client.gui.Font; @@ -53,6 +54,10 @@ public static Component binding(InputBinding binding) { return binding(binding.id()); } + public static Component binding(InputBindingSupplier bindingSupplier) { + return binding(bindingSupplier.bindId()); + } + public static int getComponentHeight(Font font, FormattedCharSequence text) { MutableInt mutableInt = new MutableInt(); text.accept((index, style, codePoint) -> { diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java index 51ba1944..0ad7399a 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java @@ -15,7 +15,6 @@ import dev.isxander.controlify.controller.input.DeadzoneGroup; import dev.isxander.controlify.controller.input.InputComponent; import dev.isxander.controlify.controller.input.Inputs; -import dev.isxander.controlify.controller.keyboard.NativeKeyboardComponent; import dev.isxander.controlify.controller.rumble.RumbleComponent; import dev.isxander.controlify.gui.controllers.BindController; import dev.isxander.controlify.gui.controllers.Deadzone2DImageRenderer; @@ -258,19 +257,13 @@ private Optional makeAccessibilityGroup(ControllerEntity controller .binding(def.showScreenGuides, () -> config.showScreenGuides, v -> config.showScreenGuides = v) .controller(TickBoxControllerBuilder::create) .build()) - .option(Option.createBuilder() + .option(Option.createBuilder() .name(Component.translatable("controlify.gui.show_keyboard")) .description(OptionDescription.createBuilder() .text(Component.translatable("controlify.gui.show_keyboard.tooltip")) .build()) - .binding( - OnScreenKeyboardMode.getForController(controller), - () -> OnScreenKeyboardMode.getForController(controller), - v -> v.setForController(controller) - ) - .controller(opt -> CyclingListControllerBuilder.create(opt) - .values(OnScreenKeyboardMode.valuesForController(controller)) - .formatValue(OnScreenKeyboardMode::getDisplayName)) + .binding(def.showOnScreenKeyboard, () -> config.showOnScreenKeyboard, v -> config.showOnScreenKeyboard = v) + .controller(TickBoxControllerBuilder::create) .build()) .build()); } @@ -733,60 +726,4 @@ private static ResourceLocation screenshot(String filename) { private record OptionBindPair(Option option, InputBinding binding) { } - - private enum OnScreenKeyboardMode implements NameableEnum { - OFF("controlify.gui.show_keyboard.off"), - CONTROLIFY("controlify.gui.show_keyboard.controlify"), - SYSTEM("controlify.gui.show_keyboard.system"); - - public static OnScreenKeyboardMode[] valuesForController(ControllerEntity controller) { - if (controller.nativeKeyboard().isPresent()) { - return new OnScreenKeyboardMode[]{ OFF, CONTROLIFY, SYSTEM }; - } else { - return new OnScreenKeyboardMode[]{ OFF, CONTROLIFY }; - } - } - - public static OnScreenKeyboardMode getForController(ControllerEntity controller) { - if (controller.genericConfig().config().showOnScreenKeyboard) { - if (controller.nativeKeyboard().map(nativeKeyboard -> nativeKeyboard.confObj().useNativeKeyboard).orElse(false)) { - return SYSTEM; - } else { - return CONTROLIFY; - } - } else { - return OFF; - } - } - - public void setForController(ControllerEntity controller) { - GenericControllerConfig genericConfig = controller.genericConfig().config(); - Optional nativeConfig = controller.nativeKeyboard().map(NativeKeyboardComponent::confObj); - switch (this) { - case OFF -> { - genericConfig.showOnScreenKeyboard = false; - nativeConfig.ifPresent(config -> config.useNativeKeyboard = false); - } - case CONTROLIFY -> { - genericConfig.showOnScreenKeyboard = true; - nativeConfig.ifPresent(config -> config.useNativeKeyboard = false); - } - case SYSTEM -> { - genericConfig.showOnScreenKeyboard = true; - nativeConfig.ifPresent(config -> config.useNativeKeyboard = true); - } - } - } - - private final Component displayName; - - OnScreenKeyboardMode(String key) { - this.displayName = Component.translatable(key); - } - - @Override - public Component getDisplayName() { - return displayName; - } - } } diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java index 68f2052f..ec2da51a 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java @@ -1,10 +1,20 @@ package dev.isxander.controlify.mixins.feature.screenkeyboard; +import com.llamalad7.mixinextras.expression.Definition; +import com.llamalad7.mixinextras.expression.Expression; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.mojang.blaze3d.platform.InputConstants; import dev.isxander.controlify.api.ControlifyApi; -import dev.isxander.controlify.controller.keyboard.NativeKeyboardComponent; -import dev.isxander.controlify.screenkeyboard.*; +import dev.isxander.controlify.bindings.ControlifyBindings; +import dev.isxander.controlify.font.BindingFontHelper; +import dev.isxander.controlify.screenkeyboard.ChatKeyboardDucky; +import dev.isxander.controlify.screenkeyboard.KeyboardLayouts; +import dev.isxander.controlify.screenkeyboard.KeyboardWidget; +import dev.isxander.controlify.screenkeyboard.MixinInputTarget; +import dev.isxander.controlify.screenop.ScreenProcessorProvider; +import dev.isxander.controlify.screenop.compat.vanilla.ChatScreenProcessor; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.CommandSuggestions; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.screens.ChatScreen; @@ -19,20 +29,22 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.util.List; -import java.util.Optional; @Mixin(ChatScreen.class) -public abstract class ChatScreenMixin extends Screen implements MixinInputTarget, ChatKeyboardDucky { - @Unique - private KeyboardWidget keyboard; - @Unique - private float shiftChatAmt = 0f; - - @Shadow - protected EditBox input; +public abstract class ChatScreenMixin extends Screen implements ScreenProcessorProvider, MixinInputTarget, ChatKeyboardDucky { + @Unique private KeyboardWidget keyboard; + @Unique private float shiftChatAmt = 0f; + @Shadow protected EditBox input; + @Shadow CommandSuggestions commandSuggestions; + @Shadow public abstract boolean keyPressed(int keyCode, int scanCode, int modifiers); - @Shadow - public abstract boolean keyPressed(int keyCode, int scanCode, int modifiers); + @Unique + private final ChatScreenProcessor screenProcessor = new ChatScreenProcessor( + (ChatScreen) (Object) this, + () -> this.input, + () -> this.keyboard, + () -> (ChatScreenProcessor.CmdSuggestionsController) this.commandSuggestions + ); protected ChatScreenMixin(Component title) { super(title); @@ -40,21 +52,17 @@ protected ChatScreenMixin(Component title) { @Inject(method = "init", at = @At("HEAD")) private void addKeyboard(CallbackInfo ci) { + this.shiftChatAmt = 0f; + ControlifyApi.get().getCurrentController().ifPresent(c -> { - if (!ControlifyApi.get().currentInputMode().isController()) return; + // if the keyboard is already present, re-add it even if we're in kb/m mode since + // setting fullscreen will turn it to that mode + if (!ControlifyApi.get().currentInputMode().isController() && this.keyboard == null) return; if (!c.genericConfig().config().showOnScreenKeyboard) return; - Optional nativeKeyboardOpt = c.nativeKeyboard(); - if (nativeKeyboardOpt.isPresent() && nativeKeyboardOpt.get().confObj().useNativeKeyboard) { - NativeKeyboardComponent nativeKeyboard = nativeKeyboardOpt.get(); - - this.shiftChatAmt = nativeKeyboard.getKeyboardHeight(); - nativeKeyboard.open(); - } else { - this.shiftChatAmt = 0.5f; - int keyboardHeight = (int) (this.height * this.shiftChatAmt); - this.addRenderableWidget(keyboard = new KeyboardWidget(0, this.height - keyboardHeight, this.width, keyboardHeight, KeyboardLayouts.chat(), this, (ChatScreen) (Object) this)); - } + this.shiftChatAmt = 0.5f; + int keyboardHeight = (int) (this.height * this.shiftChatAmt); + this.addRenderableWidget(this.keyboard = new KeyboardWidget(0, this.height - keyboardHeight, this.width, keyboardHeight, KeyboardLayouts.chat(), this, (ChatScreen) (Object) this)); }); } @@ -63,17 +71,36 @@ private GuiEventListener modifyInitialFocus(GuiEventListener editBox) { return this.keyboard != null ? this.keyboard : editBox; } - @ModifyArg(method = "init", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/ChatScreen$1;(Lnet/minecraft/client/gui/screens/ChatScreen;Lnet/minecraft/client/gui/Font;IIIILnet/minecraft/network/chat/Component;)V"), index = 3) + @Definition(id = "height", field = "Lnet/minecraft/client/gui/screens/ChatScreen;height:I") + @Definition(id = "width", field = "Lnet/minecraft/client/gui/screens/ChatScreen;width:I") + // EditBox can't be referenced here because it's an anonymous subclass that doesn't have a concrete Class type + @Expression("new ?(?, ?, 4, @(this.height - 12), this.width - 4, 12, ?)") + @ModifyExpressionValue(method = "init", at = @At("MIXINEXTRAS:EXPRESSION")) private int modifyInputBoxY(int y) { - return (int) (y - this.height * this.shiftChatAmt); + return this.height - (int) (this.height * this.shiftChatAmt) - 12; } - @ModifyArg(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;fill(IIIII)V"), index = 1) - private int modifyInputBoxBackgroundY(int y) { - return (int) (y - this.height * this.shiftChatAmt); + @Definition(id = "fill", method = "Lnet/minecraft/client/gui/GuiGraphics;fill(IIIII)V") + @Definition(id = "height", field = "Lnet/minecraft/client/gui/screens/ChatScreen;height:I") + @Definition(id = "width", field = "Lnet/minecraft/client/gui/screens/ChatScreen;width:I") + @Expression("?.fill(2, @(this.height - 14), this.width - 2, this.height - 2, ?)") + @ModifyExpressionValue(method = "render", at = @At("MIXINEXTRAS:EXPRESSION")) + private int modifyInputBoxBackgroundTop(int y) { + return this.input.getY() - 2; } - @ModifyExpressionValue(method = "init", at = @At(value = "CONSTANT", args = "intValue=10")) + @Definition(id = "fill", method = "Lnet/minecraft/client/gui/GuiGraphics;fill(IIIII)V") + @Definition(id = "height", field = "Lnet/minecraft/client/gui/screens/ChatScreen;height:I") + @Definition(id = "width", field = "Lnet/minecraft/client/gui/screens/ChatScreen;width:I") + @Expression("?.fill(2, this.height - 14, this.width - 2, @(this.height - 2), ?)") + @ModifyExpressionValue(method = "render", at = @At("MIXINEXTRAS:EXPRESSION")) + private int modifyInputBoxBackgroundBottom(int y) { + return this.input.getBottom() - 2; + } + + @Definition(id = "CommandSuggestions", type = CommandSuggestions.class) + @Expression("new CommandSuggestions(?, ?, ?, ?, ?, ?, ?, @(10), ?, ?)") + @ModifyExpressionValue(method = "init", at = @At("MIXINEXTRAS:EXPRESSION")) private int modifyMaxSuggestionCount(int count) { return shiftChatAmt > 0 ? 8 : count; } @@ -124,4 +151,9 @@ private int modifyMaxSuggestionCount(int count) { minecraft.keyboardHandler.setClipboard(this.input.getValue()); return true; } + + @Override + public ChatScreenProcessor screenProcessor() { + return this.screenProcessor; + } } diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/CommandSuggestionsMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/CommandSuggestionsMixin.java index bd8c40ae..123e88dc 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/CommandSuggestionsMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/CommandSuggestionsMixin.java @@ -2,24 +2,51 @@ import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import dev.isxander.controlify.screenkeyboard.ChatKeyboardDucky; +import dev.isxander.controlify.screenop.compat.vanilla.ChatScreenProcessor; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.CommandSuggestions; import net.minecraft.client.gui.screens.ChatScreen; +import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; @Mixin(CommandSuggestions.class) -public class CommandSuggestionsMixin { +public class CommandSuggestionsMixin implements ChatScreenProcessor.CmdSuggestionsController { @Shadow @Final Minecraft minecraft; + @Shadow + @Nullable + private CommandSuggestions.@Nullable SuggestionsList suggestions; + @ModifyExpressionValue(method = {"renderUsage", "showSuggestions"}, at = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/screens/Screen;height:I")) private int modifyUsageHeight(int height) { if (minecraft.screen instanceof ChatScreen chat) return (int) (height * (1 - ChatKeyboardDucky.getKeyboardShiftAmount(chat))); return height; } + + @Override + public boolean controlify$cycle(int amount) { + if (this.suggestions == null) return false; + + this.suggestions.cycle(amount); + return true; + } + + @Override + public boolean controlify$useSuggestion() { + if (this.suggestions == null) return false; + + this.suggestions.useSuggestion(); + return true; + } + + @Override + public boolean controlify$hasAvailableSuggestions() { + return this.suggestions != null; + } } diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java index dde420df..c90d2c28 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayout.java @@ -71,6 +71,7 @@ public static KeyboardLayout of(float width, List... keys) { * Keys can also have an optional shortcut binding, which is used to display a shortcut for the key in the UI. *

* Each key function provides the display name of the key with {@link KeyFunction#displayName()}. + * * @param regular the regular key function, which is used when the shift is not enabled. * @param shifted the shifted key function, which is used when the shift is enabled * @param width the unit width of the key, which is multiplied by the keyboard width to determine the actual pixel width of the key @@ -229,6 +230,7 @@ record CodeFunc(List codes, Component displayName) implements KeyFuncti /** * A key code is a combination of a keycode, scancode, and modifier. + * * @param keycode is the logical key code, platform agnostic. use {@link com.mojang.blaze3d.platform.InputConstants} to get the key code. * @param scancode is the physical key code, platform specific. usually leaving blank is fine since no one ever looks at it. * @param modifier is the modifier bitset, which can be used to specify additional key modifiers like shift, ctrl, alt, etc. diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java index af95bd5c..00ad6d1c 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java @@ -65,7 +65,13 @@ public void updateLayout(KeyboardLayoutWithId layout, @Nullable String identifie k -> Objects.equals(k.getKey().identifier(), identifierToFocus) ).or(() -> findKey( oldLayoutChangerToFocus != null, - k -> k.getKeyFunction() instanceof KeyboardLayout.KeyFunction.ChangeLayoutFunc changeLayoutKey && (changeLayoutKey.layout().equals(oldLayoutChangerToFocus) || changeLayoutKey.isPreviousLayout())) + k -> { + boolean isOldLayout = k.getKeyFunction() instanceof KeyboardLayout.KeyFunction.ChangeLayoutFunc changeLayoutKey + && changeLayoutKey.layout().equals(oldLayoutChangerToFocus); + boolean isPrevLayout = k.getKeyFunction() instanceof KeyboardLayout.KeyFunction.SpecialFunc specialFunc + && specialFunc.action() == KeyboardLayout.KeyFunction.SpecialFunc.Action.PREVIOUS_LAYOUT; + return isOldLayout || isPrevLayout; + }) ); } diff --git a/src/main/java/dev/isxander/controlify/screenop/compat/AbstractSliderComponentProcessor.java b/src/main/java/dev/isxander/controlify/screenop/compat/AbstractSliderComponentProcessor.java index 0127967b..b409e3a9 100644 --- a/src/main/java/dev/isxander/controlify/screenop/compat/AbstractSliderComponentProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/compat/AbstractSliderComponentProcessor.java @@ -15,10 +15,10 @@ public abstract class AbstractSliderComponentProcessor implements ComponentProce @Override public boolean overrideControllerNavigation(ScreenProcessor screen, ControllerEntity controller) { - var left = ControlifyBindings.CYCLE_OPT_BACKWARD.on(controller).digitalNow(); - var leftPrev = ControlifyBindings.CYCLE_OPT_BACKWARD.on(controller).digitalPrev(); - var right = ControlifyBindings.CYCLE_OPT_FORWARD.on(controller).digitalNow(); - var rightPrev = ControlifyBindings.CYCLE_OPT_FORWARD.on(controller).digitalPrev(); + var left = ControlifyBindings.GUI_SECONDARY_NAVI_LEFT.on(controller).digitalNow(); + var leftPrev = ControlifyBindings.GUI_SECONDARY_NAVI_LEFT.on(controller).digitalPrev(); + var right = ControlifyBindings.GUI_SECONDARY_NAVI_RIGHT.on(controller).digitalNow(); + var rightPrev = ControlifyBindings.GUI_SECONDARY_NAVI_RIGHT.on(controller).digitalPrev(); boolean repeatEventAvailable = holdRepeatHelper.canNavigate(); diff --git a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/BundleItemSlotControllerAction.java b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/BundleItemSlotControllerAction.java index afc2ea66..7acf6cf6 100644 --- a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/BundleItemSlotControllerAction.java +++ b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/BundleItemSlotControllerAction.java @@ -4,7 +4,6 @@ import dev.isxander.controlify.Controlify; import dev.isxander.controlify.bindings.ControlifyBindings; import dev.isxander.controlify.controller.ControllerEntity; -import net.minecraft.client.Minecraft; import net.minecraft.util.Mth; import net.minecraft.world.item.BundleItem; import net.minecraft.world.item.ItemStack; @@ -21,10 +20,10 @@ public static boolean onControllerInput( if (uniqueItems > 0) { Controlify.instance().virtualMouseHandler().preventScrollingThisTick(); - boolean up = ControlifyBindings.BUNDLE_NAVI_UP.on(controller).justPressed(); - boolean down = ControlifyBindings.BUNDLE_NAVI_DOWN.on(controller).justPressed(); - boolean left = ControlifyBindings.BUNDLE_NAVI_LEFT.on(controller).justPressed(); - boolean right = ControlifyBindings.BUNDLE_NAVI_RIGHT.on(controller).justPressed(); + boolean up = ControlifyBindings.GUI_SECONDARY_NAVI_UP.on(controller).justPressed(); + boolean down = ControlifyBindings.GUI_SECONDARY_NAVI_DOWN.on(controller).justPressed(); + boolean left = ControlifyBindings.GUI_SECONDARY_NAVI_LEFT.on(controller).justPressed(); + boolean right = ControlifyBindings.GUI_SECONDARY_NAVI_RIGHT.on(controller).justPressed(); int offsetX = 0, offsetY = 0; if (up) offsetY--; diff --git a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/ChatScreenProcessor.java b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/ChatScreenProcessor.java index 709c7d57..4a760b0d 100644 --- a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/ChatScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/ChatScreenProcessor.java @@ -1,16 +1,174 @@ package dev.isxander.controlify.screenop.compat.vanilla; +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.bindings.ControlifyBindings; +import dev.isxander.controlify.controller.ControllerEntity; +import dev.isxander.controlify.font.BindingFontHelper; +import dev.isxander.controlify.screenkeyboard.KeyboardWidget; import dev.isxander.controlify.screenop.ScreenProcessor; import dev.isxander.controlify.utils.HoldRepeatHelper; +import dev.isxander.controlify.utils.LazyComponentDims; +import dev.isxander.controlify.virtualmouse.VirtualMouseHandler; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.screens.ChatScreen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; +import java.util.function.Supplier; public class ChatScreenProcessor extends ScreenProcessor { - public ChatScreenProcessor(ChatScreen screen) { + private final HoldRepeatHelper textFwdCursorHoldRepeatHelper = new HoldRepeatHelper(10, 2); + private final HoldRepeatHelper textBwdCursorHoldRepeatHelper = new HoldRepeatHelper(10, 2); + private final HoldRepeatHelper suggestionsFwdHoldRepeatHelper = new HoldRepeatHelper(10, 4); + private final HoldRepeatHelper suggestionsBwdHoldRepeatHelper = new HoldRepeatHelper(10, 4); + + private final Supplier inputSupplier; + private final Supplier<@Nullable KeyboardWidget> keyboardSupplier; + private final Supplier<@Nullable CmdSuggestionsController> suggestionsController; + + private final LazyComponentDims cursorNavigateHint = new LazyComponentDims( + Component.translatable("controlify.hint.chat_cursor_movement", + BindingFontHelper.binding(ControlifyBindings.GUI_PREV_TAB), + BindingFontHelper.binding(ControlifyBindings.GUI_NEXT_TAB)) + ); + private final LazyComponentDims commandSuggesterHint = new LazyComponentDims( + Component.translatable("controlify.hint.command_suggester", + BindingFontHelper.binding(ControlifyBindings.GUI_SECONDARY_NAVI_DOWN), + BindingFontHelper.binding(ControlifyBindings.GUI_SECONDARY_NAVI_UP), + BindingFontHelper.binding(ControlifyBindings.GUI_SECONDARY_NAVI_RIGHT)) + ); + + public ChatScreenProcessor( + ChatScreen screen, + Supplier inputSupplier, + Supplier<@Nullable KeyboardWidget> keyboardSupplier, + Supplier<@Nullable CmdSuggestionsController> suggestionsController + ) { super(screen); + this.inputSupplier = inputSupplier; + this.keyboardSupplier = keyboardSupplier; + this.suggestionsController = suggestionsController; + } + + @Override + protected void handleButtons(ControllerEntity controller) { + super.handleButtons(controller); + + if (this.textFwdCursorHoldRepeatHelper.shouldAction(ControlifyBindings.GUI_NEXT_TAB.on(controller))) { + this.inputSupplier.get().moveCursor(1, false); + + this.textFwdCursorHoldRepeatHelper.onNavigate(); + this.textBwdCursorHoldRepeatHelper.reset(); + + playFocusChangeSound(); + + this.clearChatCursorHint(controller); + } + + if (this.textBwdCursorHoldRepeatHelper.shouldAction(ControlifyBindings.GUI_PREV_TAB.on(controller))) { + this.inputSupplier.get().moveCursor(-1, false); + + this.textBwdCursorHoldRepeatHelper.onNavigate(); + this.textFwdCursorHoldRepeatHelper.reset(); + + playFocusChangeSound(); + + this.clearChatCursorHint(controller); + } + + CmdSuggestionsController suggestionsController = this.suggestionsController.get(); + if (suggestionsController != null) { + if (this.suggestionsFwdHoldRepeatHelper.shouldAction(ControlifyBindings.GUI_SECONDARY_NAVI_DOWN.on(controller))) { + if (suggestionsController.controlify$cycle(1)) { + this.suggestionsFwdHoldRepeatHelper.onNavigate(); + this.suggestionsBwdHoldRepeatHelper.reset(); + + playFocusChangeSound(); + + this.clearCommandSuggesterHint(controller); + } + } + if (this.suggestionsBwdHoldRepeatHelper.shouldAction(ControlifyBindings.GUI_SECONDARY_NAVI_UP.on(controller))) { + if (suggestionsController.controlify$cycle(-1)) { + this.suggestionsBwdHoldRepeatHelper.onNavigate(); + this.suggestionsFwdHoldRepeatHelper.reset(); + + playFocusChangeSound(); + + this.clearCommandSuggesterHint(controller); + } + } + if (ControlifyBindings.GUI_SECONDARY_NAVI_RIGHT.on(controller).justPressed()) { + if (suggestionsController.controlify$useSuggestion()) { + this.suggestionsFwdHoldRepeatHelper.reset(); + this.suggestionsBwdHoldRepeatHelper.reset(); + + playClackSound(); + + this.clearCommandSuggesterHint(controller); + } + } + } + } + + @Override + protected void render(ControllerEntity controller, GuiGraphics graphics, float tickDelta, Optional vmouse) { + if (this.keyboardSupplier.get() != null) { + var config = controller.genericConfig().config(); + + if (config.hintChatCursorMovement) { + LazyComponentDims hint = this.cursorNavigateHint; + + int x = this.inputSupplier.get().getRight() - hint.getWidth() - 2; + int y = this.inputSupplier.get().getY() - hint.getHeight(); + + graphics.drawString(minecraft.font, hint.getComponent(), x, y, 0xFFFFFFFF, true); + } + + if (config.hintCommandSuggester) { + CmdSuggestionsController suggestionsController = this.suggestionsController.get(); + + if (suggestionsController != null && suggestionsController.controlify$hasAvailableSuggestions()) { + LazyComponentDims hint = this.commandSuggesterHint; + + int x = this.screen.width - hint.getWidth() - 2; + int y = 2 + Math.max(0, hint.getHeight() - minecraft.font.lineHeight); + + graphics.drawString(minecraft.font, hint.getComponent(), x, y, 0xFFFFFFFF, true); + } + } + } + } + + private void clearChatCursorHint(ControllerEntity controller) { + if (controller.genericConfig().config().hintChatCursorMovement) { + if (!this.inputSupplier.get().getValue().isEmpty()) { + controller.genericConfig().config().hintChatCursorMovement = false; + Controlify.instance().config().save(); + } + } + } + + private void clearCommandSuggesterHint(ControllerEntity controller) { + if (controller.genericConfig().config().hintCommandSuggester) { + controller.genericConfig().config().hintCommandSuggester = false; + Controlify.instance().config().save(); + } } @Override protected HoldRepeatHelper createHoldRepeatHelper() { + // make the initial delay way less return new HoldRepeatHelper(3, 4); } + + public interface CmdSuggestionsController { + boolean controlify$cycle(int amount); + + boolean controlify$useSuggestion(); + + boolean controlify$hasAvailableSuggestions(); + } } diff --git a/src/main/java/dev/isxander/controlify/utils/LazyComponentDims.java b/src/main/java/dev/isxander/controlify/utils/LazyComponentDims.java new file mode 100644 index 00000000..d0105ae4 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/utils/LazyComponentDims.java @@ -0,0 +1,40 @@ +package dev.isxander.controlify.utils; + +import dev.isxander.controlify.font.BindingFontHelper; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.network.chat.Component; + +public class LazyComponentDims { + private final Component component; + private final Font font; + private int width = -1; + private int height = -1; + + public LazyComponentDims(Font font, Component component) { + this.component = component; + this.font = font; + } + + public LazyComponentDims(Component component) { + this(Minecraft.getInstance().font, component); + } + + public Component getComponent() { + return this.component; + } + + public int getWidth() { + if (this.width == -1) { + this.width = this.font.width(this.component); + } + return this.width; + } + + public int getHeight() { + if (this.height == -1) { + this.height = BindingFontHelper.getComponentHeight(this.font, this.component); + } + return this.height; + } +} diff --git a/src/main/resources/assets/controlify/controllers/default_bind/default.json b/src/main/resources/assets/controlify/controllers/default_bind/default.json index 87ce2ef8..c5a2e58a 100644 --- a/src/main/resources/assets/controlify/controllers/default_bind/default.json +++ b/src/main/resources/assets/controlify/controllers/default_bind/default.json @@ -198,11 +198,17 @@ "controlify:gui_navi_right": { "axis": "controlify:axis/left_stick_right" }, - "controlify:cycle_opt_forward": { - "axis": "controlify:axis/right_stick_right" + "controlify:gui_secondary_navi_up": { + "axis": "controlify:axis/right_stick_up" + }, + "controlify:gui_secondary_navi_down": { + "axis": "controlify:axis/right_stick_down" }, - "controlify:cycle_opt_backward": { + "controlify:gui_secondary_navi_left": { "axis": "controlify:axis/right_stick_left" + }, + "controlify:gui_secondary_navi_right": { + "axis": "controlify:axis/right_stick_right" } } } diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index 7337a433..ac440548 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -335,8 +335,10 @@ "controlify.binding.controlify.gui_navi_up": "GUI Navi Up", "controlify.binding.controlify.gui_navi_left": "GUI Navi Left", "controlify.binding.controlify.gui_navi_right": "GUI Navi Right", - "controlify.binding.controlify.cycle_opt_forward": "Cycle Option Forward", - "controlify.binding.controlify.cycle_opt_backward": "Cycle Option Backward", + "controlify.binding.controlify.gui_secondary_navi_down": "GUI Secondary Navi Down", + "controlify.binding.controlify.gui_secondary_navi_up": "GUI Secondary Navi Up", + "controlify.binding.controlify.gui_secondary_navi_left": "GUI Secondary Navi Left", + "controlify.binding.controlify.gui_secondary_navi_right": "GUI Secondary Navi Right", "controlify.binding.controlify.clear_binding": "Clear Binding", "controlify.binding.voicechat.ptt_hold": "Push-To-Talk (Hold)", @@ -596,5 +598,8 @@ "controlify.keyboard.special.paste": "Paste", "controlify.keyboard.special.previous_layout": "Previous Layout", - "controlify.tutorial.look.description": "Use your mouse or controller to turn" + "controlify.tutorial.look.description": "Use your mouse or controller to turn", + + "controlify.hint.chat_cursor_movement": "Use %s and %s to move the chat cursor", + "controlify.hint.command_suggester": "Use %s and %s to navigate through command suggestions, and %s to select them." } From 532e3d91e6c983582ac351c94fc38fbbb6c61c7e Mon Sep 17 00:00:00 2001 From: isxander Date: Thu, 7 Aug 2025 23:20:33 +0100 Subject: [PATCH 05/12] final touches! remake the chat keyboard layout --- .../bindings/ControlifyBindings.java | 2 +- .../screenkeyboard/KeyboardWidget.java | 4 +- .../compat/vanilla/ChatScreenProcessor.java | 2 +- .../keyboard_layout/chat/en_us.json | 77 +++---------------- 4 files changed, 15 insertions(+), 70 deletions(-) diff --git a/src/main/java/dev/isxander/controlify/bindings/ControlifyBindings.java b/src/main/java/dev/isxander/controlify/bindings/ControlifyBindings.java index b5c684f9..0e4651a3 100644 --- a/src/main/java/dev/isxander/controlify/bindings/ControlifyBindings.java +++ b/src/main/java/dev/isxander/controlify/bindings/ControlifyBindings.java @@ -110,7 +110,7 @@ public final class ControlifyBindings { public static final InputBindingSupplier PAUSE = ControlifyBindApi.get().registerBinding(builder -> builder .id("controlify", "pause") .category(GAMEPLAY_CATEGORY) - .allowedContexts(BindContext.IN_GAME) + .allowedContexts(BindContext.IN_GAME, BindContext.REGULAR_SCREEN) .radialCandidate(RadialIcons.getItem(Items.STRUCTURE_VOID))); public static final InputBindingSupplier CHANGE_PERSPECTIVE = ControlifyBindApi.get().registerBinding(builder -> builder .id("controlify", "change_perspective") diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java index 00ad6d1c..1bbc1e9d 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java @@ -4,6 +4,7 @@ import dev.isxander.controlify.bindings.ControlifyBindings; import dev.isxander.controlify.controller.ControllerEntity; import dev.isxander.controlify.screenop.ComponentProcessor; +import dev.isxander.controlify.screenop.ScreenControllerEventListener; import dev.isxander.controlify.screenop.ScreenProcessor; import dev.isxander.controlify.screenop.ScreenProcessorProvider; import dev.isxander.controlify.utils.render.Blit; @@ -94,7 +95,6 @@ private void arrangeKeys(KeyboardLayout layout) { (int) x, (int) y, (int) keyWidth, (int) keyHeight, key, this ); - ScreenProcessorProvider.provide(this.containingScreen).addEventListener(keyWidget); this.keys.add(keyWidget); @@ -133,6 +133,8 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo @Override public boolean overrideControllerButtons(ScreenProcessor screen, ControllerEntity controller) { + this.keys.forEach(k -> k.onControllerInput(controller)); + // prevent default button handling for gui press which would send enter which is most likely submit return ControlifyBindings.GUI_PRESS.on(controller).guiPressed().get(); } diff --git a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/ChatScreenProcessor.java b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/ChatScreenProcessor.java index 4a760b0d..6c9c273d 100644 --- a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/ChatScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/ChatScreenProcessor.java @@ -161,7 +161,7 @@ private void clearCommandSuggesterHint(ControllerEntity controller) { @Override protected HoldRepeatHelper createHoldRepeatHelper() { // make the initial delay way less - return new HoldRepeatHelper(3, 4); + return new HoldRepeatHelper(5, 4); } public interface CmdSuggestionsController { diff --git a/src/main/resources/assets/controlify/keyboard_layout/chat/en_us.json b/src/main/resources/assets/controlify/keyboard_layout/chat/en_us.json index e6cb8ccc..a243ae80 100644 --- a/src/main/resources/assets/controlify/keyboard_layout/chat/en_us.json +++ b/src/main/resources/assets/controlify/keyboard_layout/chat/en_us.json @@ -1,80 +1,23 @@ { - "width": 16, + "width": 12, "keys": [ [ - { "regular": " ", "width": 1 }, - ["`", "~"], - ["1", "!"], - ["2", "@"], - ["3", "#"], - ["4", "$"], - ["5", "%"], - ["6", "^"], - ["7", "&"], - ["8", "*"], - ["9", "("], - ["0", ")"], - ["-", "_"], - ["=", "+"], - { "regular": { "action": "backspace"}, "shortcut": "controlify:gui_abstract_action_1", "width": 2.0 } + ["0", "!"], ["1", "?"], ["2", "$"], ["3", "#"], ["4", "-"], ["5", "_"], ["6", "+"], ["7", "("], ["8", ")"], ["9", "\""], + { "regular": { "action": "shift_lock" }, "width": 2.0 } ], [ - { "regular": " ", "width": 2 }, - "q", - "w", - "e", - "r", - "t", - "y", - "u", - "i", - "o", - "p", - ["[", "{"], - ["]", "}"], - ["\\", "|"], - " " + "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", + { "regular": { "action": "backspace" }, "shortcut": "controlify:gui_abstract_action_1", "width": 2.0 } ], [ - { "regular": { "action": "shift_lock" }, "width": 2.0 }, - "a", - "s", - "d", - "f", - "g", - "h", - "j", - "k", - "l", - [";", ":"], - ["'", "\""], - { "regular": { "action": "enter", "shortcut": "controlify:gui_abstract_action_1" }, "width": 3.0 } + ["/", "|"], + "a", "s", "d", "f", "g", "h", "j", "k", "l", + { "regular": { "action": "enter" }, "shortcut": "controlify:pause", "width": 2.0 } ], [ { "regular": { "action": "shift" }, "shortcut": "controlify:gui_abstract_action_3", "width": 2.0 }, - "z", - "x", - "c", - "v", - "b", - "n", - "m", - [",", "<"], - [".", ">"], - ["/", "?"], - { - "regular": { "action": "paste" }, - "shifted": { "action": "copy_all" }, - "width": 2.0 - }, - { "action": "left_arrow" }, - { "action": "right_arrow" } - ], - [ - { "regular": { "layout": "controlify:emoji", "display_name": "☺" }, "width": 2.0 }, - { "regular": { "chars": " ", "display_name": "Space" }, "shortcut": "controlify:gui_abstract_action_2", "width": 12.0 }, - { "action": "up_arrow" }, - { "action": "down_arrow" } + "z", "x", "c", "v", "b", "n", "m", [",", "."], + { "regular": { "chars": " ", "display_name": "Space" }, "shortcut": "controlify:gui_abstract_action_2", "width": 2.0 } ] ] } From ace5aec9e6600948198ac5df000df7c8b100d682 Mon Sep 17 00:00:00 2001 From: isxander Date: Sun, 10 Aug 2025 19:21:35 +0100 Subject: [PATCH 06/12] apply integer scaling to the keyboard when the GUI scale is so slow it makes the keys look stupid --- .../controlify/screenkeyboard/KeyWidget.java | 61 +++++++++++++------ .../screenkeyboard/KeyboardWidget.java | 4 ++ 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java index 25d3f5d0..6de02b38 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyWidget.java @@ -12,6 +12,7 @@ import dev.isxander.controlify.utils.CUtil; import dev.isxander.controlify.utils.HoldRepeatHelper; import dev.isxander.controlify.utils.render.Blit; +import dev.isxander.controlify.utils.render.CGuiPose; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; @@ -37,7 +38,10 @@ public class KeyWidget extends AbstractWidget implements ComponentProcessor, Scr private boolean buttonPressed, mousePressed; - public KeyWidget(int x, int y, int width, int height, KeyboardLayout.Key key, KeyboardWidget keyboard) { + private final int renderScale; + private final int renderWidth, renderHeight; + + public KeyWidget(int x, int y, int width, int height, int renderScale, KeyboardLayout.Key key, KeyboardWidget keyboard) { super(x, y, width, height, Component.literal("Key")); this.keyboard = keyboard; this.key = key; @@ -47,6 +51,10 @@ public KeyWidget(int x, int y, int width, int height, KeyboardLayout.Key key, Ke this.shiftedLabel = createLabel(key, true); this.supportsRegular = supportsAction(false); this.supportsShifted = supportsAction(true); + + this.renderScale = renderScale; + this.renderWidth = width / renderScale; + this.renderHeight = height / renderScale; } public void renderKeyBackground(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { @@ -57,29 +65,33 @@ public void renderKeyBackground(GuiGraphics graphics, int mouseX, int mouseY, fl this.buttonPressed = false; } - Blit.sprite(graphics, isVisuallyPressed() ? SPRITE_PRESSED : SPRITE, getX() + 1, getY() + 1, getWidth() - 2, getHeight() - 2); + doScaledRender(graphics, () -> { + Blit.sprite(graphics, isVisuallyPressed() ? SPRITE_PRESSED : SPRITE, getX() + 1, getY() + 1, renderWidth - 2, renderHeight - 2); - if (isHoveredOrFocused()) { - graphics.renderOutline(getX() - 1, getY() - 1, getWidth() + 2, getHeight() + 2, 0x80FFFFFF); - } else if (!shortcutPressed) { - this.holdRepeatHelper.reset(); - } + if (isHoveredOrFocused()) { + graphics.renderOutline(getX() - 1, getY() - 1, renderWidth + 2, renderHeight + 2, 0x80FFFFFF); + } else if (!shortcutPressed) { + this.holdRepeatHelper.reset(); + } - if (!this.active) { - // gray out the key if it does not support the action - graphics.fill(getX() + 1, getY() + 1, getX() + getWidth() - 1, getY() + getHeight() - 1, 0x30000000); - } + if (!this.active) { + // gray out the key if it does not support the action + graphics.fill(getX() + 1, getY() + 1, getX() + renderWidth - 1, getY() + renderHeight - 1, 0x30000000); + } + }); } public void renderKeyForeground(GuiGraphics graphics, int mouseX, int mouseY, float deltaTick) { - Component label = this.keyboard.isShifted() ? this.shiftedLabel : this.regularLabel; - graphics.drawCenteredString( - Minecraft.getInstance().font, - label, - getX() + getWidth() / 2, - getY() + getHeight() / 2 - 4 + (isVisuallyPressed() ? 2 : 0), - 0xFFFFFFFF - ); + doScaledRender(graphics, () -> { + Component label = this.keyboard.isShifted() ? this.shiftedLabel : this.regularLabel; + graphics.drawCenteredString( + Minecraft.getInstance().font, + label, + getX() + renderWidth / 2, + getY() + renderHeight / 2 - 4 + (isVisuallyPressed() ? 2 : 0), + 0xFFFFFFFF + ); + }); } @Override @@ -272,6 +284,17 @@ protected void updateWidgetNarration(NarrationElementOutput narrationElementOutp } + private void doScaledRender(GuiGraphics graphics, Runnable runnable) { + var pose = CGuiPose.ofPush(graphics); + pose.translate(getX(), getY()); + pose.scale(this.renderScale, this.renderScale); + pose.translate(-getX(), -getY()); + + runnable.run(); + + pose.pop(); + } + private static void insertText(String text, InputTarget inputConsumer) { // One `char` is not necessarily one visible character. // Some characters, such as emojis, are represented using surrogate pairs, diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java index 1bbc1e9d..510468c3 100644 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardWidget.java @@ -18,6 +18,7 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -85,6 +86,8 @@ private void arrangeKeys(KeyboardLayout layout) { float unitWidth = this.getWidth() / layout.width(); float keyHeight = (float) this.getHeight() / layout.keys().size(); + int renderScale = Mth.floor(Math.max(0, Math.min(unitWidth, keyHeight) / 60f)) + 1; + float y = this.getY(); for (List row : layout.keys()) { float x = this.getX(); @@ -93,6 +96,7 @@ private void arrangeKeys(KeyboardLayout layout) { var keyWidget = new KeyWidget( (int) x, (int) y, (int) keyWidth, (int) keyHeight, + renderScale, key, this ); From b7039b71ff46e1a7f7d58008563e7a944e9db46a Mon Sep 17 00:00:00 2001 From: isxander Date: Sun, 10 Aug 2025 23:29:42 +0100 Subject: [PATCH 07/12] improvements to signs --- .../AbstractSignEditScreenMixin.java | 19 ---- .../{ => chat}/ChatComponentMixin.java | 2 +- .../{ => chat}/ChatScreenMixin.java | 22 ++-- .../{ => chat}/CommandSuggestionsMixin.java | 2 +- .../screenkeyboard/editbox/EditBoxMixin.java | 23 ++++ .../sign/AbstractSignEditScreenMixin.java | 105 ++++++++++++++++++ .../sign/SignEditScreenMixin.java | 43 +++++++ .../KeyboardSupportedMarker.java | 22 ++++ .../keyboard_layout/full/en_us.json | 23 ++++ src/main/resources/controlify.mixins.json | 10 +- 10 files changed, 237 insertions(+), 34 deletions(-) delete mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/AbstractSignEditScreenMixin.java rename src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/{ => chat}/ChatComponentMixin.java (97%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/{ => chat}/ChatScreenMixin.java (92%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/{ => chat}/CommandSuggestionsMixin.java (96%) create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/editbox/EditBoxMixin.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/sign/AbstractSignEditScreenMixin.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/sign/SignEditScreenMixin.java create mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardSupportedMarker.java create mode 100644 src/main/resources/assets/controlify/keyboard_layout/full/en_us.json diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/AbstractSignEditScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/AbstractSignEditScreenMixin.java deleted file mode 100644 index 6ddca044..00000000 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/AbstractSignEditScreenMixin.java +++ /dev/null @@ -1,19 +0,0 @@ -package dev.isxander.controlify.mixins.feature.screenkeyboard; - -import dev.isxander.controlify.screenkeyboard.KeyboardWidget; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.inventory.AbstractSignEditScreen; -import net.minecraft.network.chat.Component; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; - -@Mixin(AbstractSignEditScreen.class) -public abstract class AbstractSignEditScreenMixin extends Screen { - - @Unique - private KeyboardWidget keyboard; - - protected AbstractSignEditScreenMixin(Component title) { - super(title); - } -} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatComponentMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/ChatComponentMixin.java similarity index 97% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatComponentMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/ChatComponentMixin.java index e279dc7a..b0f7cb81 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatComponentMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/ChatComponentMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenkeyboard; +package dev.isxander.controlify.mixins.feature.screenkeyboard.chat; import com.llamalad7.mixinextras.expression.Definition; import com.llamalad7.mixinextras.expression.Expression; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/ChatScreenMixin.java similarity index 92% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/ChatScreenMixin.java index ec2da51a..68aaa4cc 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/ChatScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/ChatScreenMixin.java @@ -1,19 +1,13 @@ -package dev.isxander.controlify.mixins.feature.screenkeyboard; +package dev.isxander.controlify.mixins.feature.screenkeyboard.chat; import com.llamalad7.mixinextras.expression.Definition; import com.llamalad7.mixinextras.expression.Expression; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.mojang.blaze3d.platform.InputConstants; import dev.isxander.controlify.api.ControlifyApi; -import dev.isxander.controlify.bindings.ControlifyBindings; -import dev.isxander.controlify.font.BindingFontHelper; -import dev.isxander.controlify.screenkeyboard.ChatKeyboardDucky; -import dev.isxander.controlify.screenkeyboard.KeyboardLayouts; -import dev.isxander.controlify.screenkeyboard.KeyboardWidget; -import dev.isxander.controlify.screenkeyboard.MixinInputTarget; +import dev.isxander.controlify.screenkeyboard.*; import dev.isxander.controlify.screenop.ScreenProcessorProvider; import dev.isxander.controlify.screenop.compat.vanilla.ChatScreenProcessor; -import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.CommandSuggestions; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.events.GuiEventListener; @@ -31,7 +25,7 @@ import java.util.List; @Mixin(ChatScreen.class) -public abstract class ChatScreenMixin extends Screen implements ScreenProcessorProvider, MixinInputTarget, ChatKeyboardDucky { +public abstract class ChatScreenMixin extends Screen implements ScreenProcessorProvider, MixinInputTarget, ChatKeyboardDucky, KeyboardSupportedMarker { @Unique private KeyboardWidget keyboard; @Unique private float shiftChatAmt = 0f; @Shadow protected EditBox input; @@ -66,6 +60,11 @@ private void addKeyboard(CallbackInfo ci) { }); } + @Inject(method = "init", at = @At("RETURN")) + private void markEditBoxAsSupported(CallbackInfo ci) { + KeyboardSupportedMarker.setKeyboardSupported(this.input, true); + } + @ModifyArg(method = "setInitialFocus", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/ChatScreen;setInitialFocus(Lnet/minecraft/client/gui/components/events/GuiEventListener;)V")) private GuiEventListener modifyInitialFocus(GuiEventListener editBox) { return this.keyboard != null ? this.keyboard : editBox; @@ -156,4 +155,9 @@ private int modifyMaxSuggestionCount(int count) { public ChatScreenProcessor screenProcessor() { return this.screenProcessor; } + + @Override + public boolean controlify$isKeyboardSupported() { + return this.keyboard != null; + } } diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/CommandSuggestionsMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/CommandSuggestionsMixin.java similarity index 96% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/CommandSuggestionsMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/CommandSuggestionsMixin.java index 123e88dc..b4e9592b 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/CommandSuggestionsMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/CommandSuggestionsMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenkeyboard; +package dev.isxander.controlify.mixins.feature.screenkeyboard.chat; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import dev.isxander.controlify.screenkeyboard.ChatKeyboardDucky; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/editbox/EditBoxMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/editbox/EditBoxMixin.java new file mode 100644 index 00000000..e8b6a6a5 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/editbox/EditBoxMixin.java @@ -0,0 +1,23 @@ +package dev.isxander.controlify.mixins.feature.screenkeyboard.editbox; + +import dev.isxander.controlify.screenkeyboard.KeyboardSupportedMarker; +import net.minecraft.client.gui.components.EditBox; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(EditBox.class) +public class EditBoxMixin implements KeyboardSupportedMarker.Mutable { + + @Unique + private boolean keyboardSupported = false; + + @Override + public void controlify$setKeyboardSupported(boolean supported) { + this.keyboardSupported = supported; + } + + @Override + public boolean controlify$isKeyboardSupported() { + return this.keyboardSupported; + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/sign/AbstractSignEditScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/sign/AbstractSignEditScreenMixin.java new file mode 100644 index 00000000..689cca9a --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/sign/AbstractSignEditScreenMixin.java @@ -0,0 +1,105 @@ +package dev.isxander.controlify.mixins.feature.screenkeyboard.sign; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.mojang.blaze3d.platform.InputConstants; +import dev.isxander.controlify.api.ControlifyApi; +import dev.isxander.controlify.screenkeyboard.KeyboardLayouts; +import dev.isxander.controlify.screenkeyboard.KeyboardSupportedMarker; +import dev.isxander.controlify.screenkeyboard.KeyboardWidget; +import dev.isxander.controlify.screenkeyboard.MixinInputTarget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.font.TextFieldHelper; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractSignEditScreen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(AbstractSignEditScreen.class) +public abstract class AbstractSignEditScreenMixin extends Screen implements MixinInputTarget, KeyboardSupportedMarker { + + @Shadow + private @Nullable TextFieldHelper signField; + @Shadow + @Final + private String[] messages; + @Shadow + private int line; + + @Shadow + protected abstract void onDone(); + + @Unique + private KeyboardWidget keyboard; + + protected AbstractSignEditScreenMixin(Component title) { + super(title); + } + + @Inject(method = "init", at = @At("HEAD")) + private void addKeyboard(CallbackInfo ci) { + ControlifyApi.get().getCurrentController().ifPresent(c -> { + // if the keyboard is already present, re-add it even if we're in kb/m mode since + // setting fullscreen will turn it to that mode + if (!ControlifyApi.get().currentInputMode().isController() && this.keyboard == null) return; + if (!c.genericConfig().config().showOnScreenKeyboard) return; + + int keyboardHeight = (int) (this.height * 0.5f); + this.addRenderableWidget(this.keyboard = new KeyboardWidget(0, this.height - keyboardHeight, this.width, keyboardHeight, KeyboardLayouts.chat(), this, (AbstractSignEditScreen) (Object) this)); + }); + } + + @WrapWithCondition(method = "init", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/AbstractSignEditScreen;addRenderableWidget(Lnet/minecraft/client/gui/components/events/GuiEventListener;)Lnet/minecraft/client/gui/components/events/GuiEventListener;")) + private boolean shouldAddDoneButton(AbstractSignEditScreen instance, GuiEventListener guiEventListener) { + return this.keyboard == null; + } + + @Override + public boolean controlify$supportsCharInput() { + return true; + } + + @Override + public boolean controlify$acceptChar(char ch, int modifiers) { + return this.signField != null && this.signField.charTyped(ch); + } + + @Override + public boolean controlify$supportsKeyCodeInput() { + return true; + } + + @Override + public boolean controlify$acceptKeyCode(int keycode, int scancode, int modifiers) { + if (keycode == InputConstants.KEY_RETURN) { + this.onDone(); + return true; + } + + return this.signField != null && this.signField.keyPressed(keycode); + } + + @Override + public boolean controlify$supportsCopying() { + return true; + } + + @Override + public boolean controlify$copy() { + if (this.signField == null) return false; + + minecraft.keyboardHandler.setClipboard(this.messages[this.line]); + return true; + } + + @Override + public boolean controlify$isKeyboardSupported() { + return this.keyboard != null; + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/sign/SignEditScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/sign/SignEditScreenMixin.java new file mode 100644 index 00000000..a948c27d --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/sign/SignEditScreenMixin.java @@ -0,0 +1,43 @@ +package dev.isxander.controlify.mixins.feature.screenkeyboard.sign; + +import com.llamalad7.mixinextras.expression.Definition; +import com.llamalad7.mixinextras.expression.Expression; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import dev.isxander.controlify.screenkeyboard.KeyboardSupportedMarker; +import net.minecraft.client.gui.screens.inventory.AbstractSignEditScreen; +import net.minecraft.client.gui.screens.inventory.SignEditScreen; +import net.minecraft.world.level.block.entity.SignBlockEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(SignEditScreen.class) +public abstract class SignEditScreenMixin extends AbstractSignEditScreen implements KeyboardSupportedMarker { + + public SignEditScreenMixin(SignBlockEntity sign, boolean isFrontText, boolean isFiltered) { + super(sign, isFrontText, isFiltered); + } + + @ModifyReturnValue(method = "getSignYOffset", at = @At("RETURN")) + private float modifySignY(float original) { + return original - calculateOverlap(); + } + + @Definition(id = "submitSignRenderState", method = "Lnet/minecraft/client/gui/GuiGraphics;submitSignRenderState(Lnet/minecraft/client/model/Model$Simple;FLnet/minecraft/world/level/block/state/properties/WoodType;IIII)V") + @Expression("?.submitSignRenderState(?, ?, ?, ?, @(66), ?, @(168))") + @ModifyExpressionValue(method = "renderSignBackground", at = @At("MIXINEXTRAS:EXPRESSION")) + private int modifySignRenderY(int original) { + return (int) (original - calculateOverlap()); + } + + @Unique + private float calculateOverlap() { + float original = 90f; + + float keyboardStart = this.height / 2f; + float signEnd = original + 90; + return Math.max(0, signEnd - keyboardStart); + } + +} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardSupportedMarker.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardSupportedMarker.java new file mode 100644 index 00000000..1148e090 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardSupportedMarker.java @@ -0,0 +1,22 @@ +package dev.isxander.controlify.screenkeyboard; + +public interface KeyboardSupportedMarker { + + boolean controlify$isKeyboardSupported(); + + static boolean isKeyboardSupported(Object obj) { + return obj instanceof KeyboardSupportedMarker marker && marker.controlify$isKeyboardSupported(); + } + + static boolean setKeyboardSupported(Object obj, boolean supported) { + if (obj instanceof Mutable mutable) { + mutable.controlify$setKeyboardSupported(supported); + return true; + } + return false; + } + + interface Mutable extends KeyboardSupportedMarker { + void controlify$setKeyboardSupported(boolean supported); + } +} diff --git a/src/main/resources/assets/controlify/keyboard_layout/full/en_us.json b/src/main/resources/assets/controlify/keyboard_layout/full/en_us.json new file mode 100644 index 00000000..a243ae80 --- /dev/null +++ b/src/main/resources/assets/controlify/keyboard_layout/full/en_us.json @@ -0,0 +1,23 @@ +{ + "width": 12, + "keys": [ + [ + ["0", "!"], ["1", "?"], ["2", "$"], ["3", "#"], ["4", "-"], ["5", "_"], ["6", "+"], ["7", "("], ["8", ")"], ["9", "\""], + { "regular": { "action": "shift_lock" }, "width": 2.0 } + ], + [ + "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", + { "regular": { "action": "backspace" }, "shortcut": "controlify:gui_abstract_action_1", "width": 2.0 } + ], + [ + ["/", "|"], + "a", "s", "d", "f", "g", "h", "j", "k", "l", + { "regular": { "action": "enter" }, "shortcut": "controlify:pause", "width": 2.0 } + ], + [ + { "regular": { "action": "shift" }, "shortcut": "controlify:gui_abstract_action_3", "width": 2.0 }, + "z", "x", "c", "v", "b", "n", "m", [",", "."], + { "regular": { "chars": " ", "display_name": "Space" }, "shortcut": "controlify:gui_abstract_action_2", "width": 2.0 } + ] + ] +} diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json index 1080e5eb..77adb00f 100644 --- a/src/main/resources/controlify.mixins.json +++ b/src/main/resources/controlify.mixins.json @@ -63,10 +63,12 @@ "feature.rumble.useitem.LocalPlayerMixin", "feature.rumble.waterland.LocalPlayerMixin", "feature.rumble.waterland.PlayerMixin", - "feature.screenkeyboard.AbstractSignEditScreenMixin", - "feature.screenkeyboard.ChatComponentMixin", - "feature.screenkeyboard.ChatScreenMixin", - "feature.screenkeyboard.CommandSuggestionsMixin", + "feature.screenkeyboard.chat.ChatComponentMixin", + "feature.screenkeyboard.chat.ChatScreenMixin", + "feature.screenkeyboard.chat.CommandSuggestionsMixin", + "feature.screenkeyboard.editbox.EditBoxMixin", + "feature.screenkeyboard.sign.AbstractSignEditScreenMixin", + "feature.screenkeyboard.sign.SignEditScreenMixin", "feature.screenop.ContainerEventHandlerMixin", "feature.screenop.MinecraftMixin", "feature.screenop.ScreenAccessor", From 4c07d7c73714cb1420fa005ef1346ea89bf5cb54 Mon Sep 17 00:00:00 2001 From: isxander Date: Fri, 15 Aug 2025 18:20:19 +0100 Subject: [PATCH 08/12] documentation, keyboards on all edit boxes, refactor screenop mixins, fix bugs with controller navigation in select world + server list screens, --- docs/reference/_meta.json | 2 +- docs/reference/builtin-bindings.mdx | 188 ++++++++------- docs/resource-packs/_meta.json | 3 +- docs/resource-packs/keyboard-layouts.mdx | 183 ++++++++++++++ docs/schemas/keyboard_layout.json | 141 +++++++++++ .../dev/isxander/controlify/Controlify.java | 3 +- .../StringControllerElementAccessor.java | 11 + .../mixins/StringControllerElementMixin.java | 21 ++ ...ngControllerElementComponentProcessor.java | 79 ++++++ .../controller/GenericControllerConfig.java | 5 +- .../bookfocusfix}/BookEditScreenMixin.java | 2 +- .../screenkeyboard/editbox/EditBoxMixin.java | 23 -- .../feature/screenop/MinecraftMixin.java | 8 + .../bundle/BundleMouseActionsMixin.java | 7 +- .../impl}/chat/ChatComponentMixin.java | 4 +- .../impl}/chat/ChatScreenMixin.java | 38 +-- .../impl}/chat/CommandSuggestionsMixin.java | 4 +- .../AbstractContainerScreenMixin.java | 2 +- .../AbstractRecipeBookScreenMixin.java | 2 +- .../CreativeModeInventoryScreenMixin.java | 5 +- .../container}/MerchantScreenMixin.java | 2 +- .../elements}/AbstractButtonMixin.java | 2 +- .../AbstractContainerEventHandlerMixin.java | 2 +- .../elements}/AbstractSelectionListMixin.java | 3 +- .../elements}/AbstractSliderButtonMixin.java | 2 +- ...ontainerObjectSelectionListEntryMixin.java | 2 +- .../screenop/impl/elements/EditBoxMixin.java | 105 ++++++++ .../outofgame}/CreateWorldScreenMixin.java | 2 +- .../DirectJoinServerScreenMixin.java | 48 ++++ .../impl/outofgame/EditServerScreenMixin.java | 48 ++++ .../JoinMultiplayerScreenAccessor.java | 2 +- .../outofgame/JoinMultiplayerScreenMixin.java | 68 ++++++ .../LanguageSelectionListEntryMixin.java | 2 +- .../outofgame}/OptionsSubScreenAccessor.java | 2 +- .../outofgame}/PauseScreenAccessor.java | 2 +- .../outofgame}/PauseScreenMixin.java | 3 +- .../outofgame}/SelectWorldScreenAccessor.java | 2 +- .../outofgame}/SelectWorldScreenMixin.java | 2 +- .../ServerSelectionListEntryMixin.java | 2 +- .../outofgame}/TabNavigationBarAccessor.java | 2 +- .../outofgame}/TitleScreenMixin.java | 2 +- .../WorldSelectionListEntryMixin.java | 2 +- .../sign/AbstractSignEditScreenMixin.java | 63 +++-- .../impl}/sign/SignEditScreenMixin.java | 5 +- .../vanilla/AbstractSignEditScreenMixin.java | 19 -- .../screenop/vanilla/EditBoxMixin.java | 18 -- .../vanilla/JoinMultiplayerScreenMixin.java | 23 -- .../screenkeyboard/KeyboardLayoutWithId.java | 6 - .../screenkeyboard/KeyboardLayouts.java | 20 -- .../KeyboardSupportedMarker.java | 22 -- .../screenop/ComponentProcessor.java | 5 + .../screenop/ComponentProcessorProvider.java | 4 + .../controlify/screenop/ScreenProcessor.java | 33 ++- .../AbstractSignEditScreenProcessor.java | 44 +++- .../vanilla/AddServerLikeScreenProcessor.java | 78 ++++++ .../compat/vanilla/ChatScreenProcessor.java | 51 ++-- .../vanilla/CreateWorldScreenProcessor.java | 2 +- .../vanilla/EditBoxComponentProcessor.java | 92 +++++++ .../JoinMultiplayerScreenProcessor.java | 79 ++++-- ...nguageSelectionListComponentProcessor.java | 2 +- .../compat/vanilla/PauseScreenProcessor.java | 2 +- .../vanilla/SelectWorldScreenProcessor.java | 6 +- ...rSelectionListEntryComponentProcessor.java | 4 +- .../WorldListEntryComponentProcessor.java | 4 +- .../keyboard}/ChatKeyboardDucky.java | 2 +- .../keyboard/ComponentKeyboardBehaviour.java | 36 +++ .../keyboard}/FallbackKeyboardLayout.java | 10 +- .../keyboard}/InputTarget.java | 16 +- .../keyboard}/KeyWidget.java | 35 ++- .../keyboard}/KeyboardLayout.java | 2 +- .../keyboard}/KeyboardLayoutManager.java | 5 +- .../keyboard/KeyboardLayoutWithId.java | 13 + .../screenop/keyboard/KeyboardLayouts.java | 36 +++ .../keyboard/KeyboardOverlayScreen.java | 227 ++++++++++++++++++ .../keyboard}/KeyboardWidget.java | 39 ++- .../keyboard}/MixinInputTarget.java | 20 +- .../controlify/utils/LazyComponentDims.java | 21 +- .../keyboard_layout/server_ip/en_us.json | 21 ++ .../{chat => simple}/en_us.json | 0 .../assets/controlify/lang/en_us.json | 5 +- .../controlify-compat.yacl.mixins.json | 4 +- src/main/resources/controlify.mixins.json | 64 ++--- 82 files changed, 1749 insertions(+), 427 deletions(-) create mode 100644 docs/resource-packs/keyboard-layouts.mdx create mode 100644 docs/schemas/keyboard_layout.json create mode 100644 src/main/java/dev/isxander/controlify/compatibility/yacl/mixins/StringControllerElementAccessor.java create mode 100644 src/main/java/dev/isxander/controlify/compatibility/yacl/mixins/StringControllerElementMixin.java create mode 100644 src/main/java/dev/isxander/controlify/compatibility/yacl/screenop/StringControllerElementComponentProcessor.java rename src/main/java/dev/isxander/controlify/mixins/feature/{screenop/vanilla => patches/bookfocusfix}/BookEditScreenMixin.java (94%) delete mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/editbox/EditBoxMixin.java rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl}/bundle/BundleMouseActionsMixin.java (87%) rename src/main/java/dev/isxander/controlify/mixins/feature/{screenkeyboard => screenop/impl}/chat/ChatComponentMixin.java (94%) rename src/main/java/dev/isxander/controlify/mixins/feature/{screenkeyboard => screenop/impl}/chat/ChatScreenMixin.java (90%) rename src/main/java/dev/isxander/controlify/mixins/feature/{screenkeyboard => screenop/impl}/chat/CommandSuggestionsMixin.java (92%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/container}/AbstractContainerScreenMixin.java (98%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/container}/AbstractRecipeBookScreenMixin.java (96%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/container}/CreativeModeInventoryScreenMixin.java (80%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/container}/MerchantScreenMixin.java (97%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/elements}/AbstractButtonMixin.java (91%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/elements}/AbstractContainerEventHandlerMixin.java (89%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/elements}/AbstractSelectionListMixin.java (85%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/elements}/AbstractSliderButtonMixin.java (95%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/elements}/ContainerObjectSelectionListEntryMixin.java (89%) create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/EditBoxMixin.java rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/outofgame}/CreateWorldScreenMixin.java (97%) create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/DirectJoinServerScreenMixin.java create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/EditServerScreenMixin.java rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/outofgame}/JoinMultiplayerScreenAccessor.java (82%) create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/JoinMultiplayerScreenMixin.java rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/outofgame}/LanguageSelectionListEntryMixin.java (93%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/outofgame}/OptionsSubScreenAccessor.java (81%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/outofgame}/PauseScreenAccessor.java (78%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/outofgame}/PauseScreenMixin.java (89%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/outofgame}/SelectWorldScreenAccessor.java (86%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/outofgame}/SelectWorldScreenMixin.java (97%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/outofgame}/ServerSelectionListEntryMixin.java (92%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/outofgame}/TabNavigationBarAccessor.java (87%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/outofgame}/TitleScreenMixin.java (90%) rename src/main/java/dev/isxander/controlify/mixins/feature/screenop/{vanilla => impl/outofgame}/WorldSelectionListEntryMixin.java (91%) rename src/main/java/dev/isxander/controlify/mixins/feature/{screenkeyboard => screenop/impl}/sign/AbstractSignEditScreenMixin.java (61%) rename src/main/java/dev/isxander/controlify/mixins/feature/{screenkeyboard => screenop/impl}/sign/SignEditScreenMixin.java (90%) delete mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractSignEditScreenMixin.java delete mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/EditBoxMixin.java delete mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/JoinMultiplayerScreenMixin.java delete mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutWithId.java delete mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayouts.java delete mode 100644 src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardSupportedMarker.java create mode 100644 src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AddServerLikeScreenProcessor.java rename src/main/java/dev/isxander/controlify/{screenkeyboard => screenop/keyboard}/ChatKeyboardDucky.java (84%) create mode 100644 src/main/java/dev/isxander/controlify/screenop/keyboard/ComponentKeyboardBehaviour.java rename src/main/java/dev/isxander/controlify/{screenkeyboard => screenop/keyboard}/FallbackKeyboardLayout.java (88%) rename src/main/java/dev/isxander/controlify/{screenkeyboard => screenop/keyboard}/InputTarget.java (53%) rename src/main/java/dev/isxander/controlify/{screenkeyboard => screenop/keyboard}/KeyWidget.java (94%) rename src/main/java/dev/isxander/controlify/{screenkeyboard => screenop/keyboard}/KeyboardLayout.java (99%) rename src/main/java/dev/isxander/controlify/{screenkeyboard => screenop/keyboard}/KeyboardLayoutManager.java (97%) create mode 100644 src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardLayoutWithId.java create mode 100644 src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardLayouts.java create mode 100644 src/main/java/dev/isxander/controlify/screenop/keyboard/KeyboardOverlayScreen.java rename src/main/java/dev/isxander/controlify/{screenkeyboard => screenop/keyboard}/KeyboardWidget.java (85%) rename src/main/java/dev/isxander/controlify/{screenkeyboard => screenop/keyboard}/MixinInputTarget.java (68%) create mode 100644 src/main/resources/assets/controlify/keyboard_layout/server_ip/en_us.json rename src/main/resources/assets/controlify/keyboard_layout/{chat => simple}/en_us.json (100%) diff --git a/docs/reference/_meta.json b/docs/reference/_meta.json index e1bc71e0..fcee6fc8 100644 --- a/docs/reference/_meta.json +++ b/docs/reference/_meta.json @@ -1,5 +1,5 @@ { "builtin-bindings.mdx": "Built-in bindings", "builtin-inputs.mdx": "Built-in inputs", - "builtin-controller-namespaces": "Built-in controller namespaces", + "builtin-controller-namespaces.mdx": "Built-in controller namespaces" } diff --git a/docs/reference/builtin-bindings.mdx b/docs/reference/builtin-bindings.mdx index fa85c8bc..feeb322e 100644 --- a/docs/reference/builtin-bindings.mdx +++ b/docs/reference/builtin-bindings.mdx @@ -2,105 +2,125 @@ title: Built-in Bindings --- -This is a list of all Controlify's built-in bindings. +This is a list of all Controlify's built-in bindings with brief descriptions and availability. These are the entries on the 'Controls' page of controller settings. ### Movement -- `controlify:walk_forward` -- `controlify:walk_backward` -- `controlify:strafe_left` -- `controlify:strafe_right` -- `controlify:look_up` -- `controlify:look_down` -- `controlify:look_left` -- `controlify:look_right` -- `controlify:gyro_button` -- `controlify:jump` -- `controlify:sprint` -- `controlify:sneak` + +| ID | Description | Availability | +|----------------------------|----------------------------|----------------------------------| +| `controlify:walk_forward` | Move forward | | +| `controlify:walk_backward` | Move backward | | +| `controlify:strafe_left` | Strafe left | | +| `controlify:strafe_right` | Strafe right | | +| `controlify:look_up` | Look up | | +| `controlify:look_down` | Look down | | +| `controlify:look_left` | Look left | | +| `controlify:look_right` | Look right | | +| `controlify:gyro_button` | Hold to enable gyro aiming | Requires gyro-capable controller | +| `controlify:jump` | Jump | | +| `controlify:sprint` | Sprint | | +| `controlify:sneak` | Sneak / crouch | | ### Gameplay -- `controlify:attack` (equivalent to left-click) -- `controlify:use` (equivalent to right-click) -- `controlify:drop` -- `controlify:drop_stack` -- `controlify:pause` -- `controlify:change_perspective` -- `controlify:swap_hands` -- `controlify:next_slot` (hotbar) -- `controlify:prev_slot` (hotbar) -- `controlify:hotbar_item_select_radial` -- `controlify:game_mode_switcher` + +| ID | Description | Availability | +|----------------------------------------|--------------------------------|--------------| +| `controlify:attack` | Primary action (left-click) | | +| `controlify:use` | Secondary action (right-click) | | +| `controlify:drop` | Drop selected item | | +| `controlify:drop_stack` | Drop entire stack | | +| `controlify:pause` | Pause / open menu | | +| `controlify:change_perspective` | Cycle camera perspective | | +| `controlify:swap_hands` | Swap main/off-hand items | | +| `controlify:next_slot` | Select next hotbar slot | | +| `controlify:prev_slot` | Select previous hotbar slot | | +| `controlify:hotbar_item_select_radial` | Open hotbar selection radial | | +| `controlify:game_mode_switcher` | Open game mode switcher | | ### Inventory -- `controlify:inventory` -- `controlify:inv_select` -- `controlify:inv_quick_move` -- `controlify:inv_take_half` -- `controlify:drop_inventory` -- `controlify:bundle_navi_up` -- `controlify:bundle_navi_down` -- `controlify:bundle_navi_left` -- `controlify:bundle_navi_right` + +| ID | Description | Availability | +|-----------------------------|----------------------------|--------------| +| `controlify:inventory` | Open inventory | | +| `controlify:inv_select` | Select slot / pick up item | | +| `controlify:inv_quick_move` | Quick-move (shift-click) | | +| `controlify:inv_take_half` | Take half from stack | | +| `controlify:drop_inventory` | Drop from inventory | | ### Creative -- `controlify:pick_block` -- `controlify:pick_block_nbt` -- `controlify:hotbar_load_radial` -- `controlify:hotbar_save_radial` + +| ID | Description | Availability | +|---------------------------------|----------------------------|---------------| +| `controlify:pick_block` | Pick block under crosshair | | +| `controlify:pick_block_nbt` | Pick block with NBT | Creative only | +| `controlify:hotbar_load_radial` | Load saved hotbar radial | Creative only | +| `controlify:hotbar_save_radial` | Save hotbar radial | Creative only | ### Misc -- `controlify:open_chat` -- `controlify:toggle_hud_visibility` -- `controlify:show_player_list` -- `controlify:take_screenshot` -- `controlify:debug_radial` -- `controlify:toggle_debug_menu` -- `controlify:toggle_debug_menu_fps` -- `controlify:toggle_debug_menu_net` (>= 1.20.3 targets only) -- `controlify:toggle_debug_menu_prof` (>= 1.20.3 targets only) -- `controlify:toggle_debug_menu_charts` (< 1.20.3 targets only) + +| ID | Description | Availability | +|-------------------------------------|-------------------------------|----------------------| +| `controlify:open_chat` | Open chat | | +| `controlify:toggle_hud_visibility` | Toggle HUD visibility | | +| `controlify:show_player_list` | Show player list | | +| `controlify:take_screenshot` | Take screenshot | | +| `controlify:debug_radial` | Open debug radial | | +| `controlify:toggle_debug_menu` | Toggle debug screen | | +| `controlify:toggle_debug_menu_fps` | Toggle debug (FPS) panel | | +| `controlify:toggle_debug_menu_net` | Toggle debug (Network) panel | 1.20.3+ targets only | +| `controlify:toggle_debug_menu_prof` | Toggle debug (Profiler) panel | 1.20.3+ targets only | ### GUI -- `controlify:gui_press` -- `controlify:gui_back` -- `controlify:gui_next_tab` -- `controlify:gui_prev_tab` -- `controlify:gui_abstract_action_1` -- `controlify:gui_abstract_action_2` -- `controlify:gui_abstract_action_3` -- `controlify:gui_navi_up` -- `controlify:gui_navi_down` -- `controlify:gui_navi_left` -- `controlify:gui_navi_right` -- `controlify:cycle_opt_forward` -- `controlify:cycle_opt_backward` + +| ID | Description | Availability | +|---------------------------------------|----------------------------------|--------------| +| `controlify:gui_press` | Activate / press focused control | | +| `controlify:gui_back` | Go back / close | | +| `controlify:gui_next_tab` | Next tab / page | | +| `controlify:gui_prev_tab` | Previous tab / page | | +| `controlify:gui_abstract_action_1` | Context action 1 | | +| `controlify:gui_abstract_action_2` | Context action 2 | | +| `controlify:gui_abstract_action_3` | Context action 3 | | +| `controlify:gui_navi_up` | Navigate focus up | | +| `controlify:gui_navi_down` | Navigate focus down | | +| `controlify:gui_navi_left` | Navigate focus left | | +| `controlify:gui_navi_right` | Navigate focus right | | +| `controlify:gui_secondary_navi_up` | Navigate secondary area up | | +| `controlify:gui_secondary_navi_down` | Navigate secondary area down | | +| `controlify:gui_secondary_navi_left` | Navigate secondary area left | | +| `controlify:gui_secondary_navi_right` | Navigate secondary area right | | ### Radial Menu -- `controlify:radial_menu` -- `controlify:radial_axis_up` -- `controlify:radial_axis_down` -- `controlify:radial_axis_left` -- `controlify:radial_axis_right` + +| ID | Description | Availability | +|--------------------------------|-----------------------------|--------------| +| `controlify:radial_menu` | Open the radial menu | | +| `controlify:radial_axis_up` | Select radial segment up | | +| `controlify:radial_axis_down` | Select radial segment down | | +| `controlify:radial_axis_left` | Select radial segment left | | +| `controlify:radial_axis_right` | Select radial segment right | | ### Virtual Mouse -- `controlify:vmouse_move_up` -- `controlify:vmouse_move_down` -- `controlify:vmouse_move_left` -- `controlify:vmouse_move_right` -- `controlify:vmouse_snap_up` -- `controlify:vmouse_snap_down` -- `controlify:vmouse_snap_left` -- `controlify:vmouse_snap_right` -- `controlify:vmouse_lclick` -- `controlify:vmouse_rclick` -- `controlify:vmouse_shift_click` -- `controlify:vmouse_scroll_down` -- `controlify:vmouse_scroll_up` -- `controlify:vmouse_shift` -- `controlify:vmouse_page_next` -- `controlify:vmouse_page_prev` -- `controlify:vmouse_page_down` -- `controlify:vmouse_page_up` -- `controlify:vmouse_toggle` +| ID | Description | Availability | +|---------------------------------|-------------------------------|--------------| +| `controlify:vmouse_move_up` | Move cursor up | | +| `controlify:vmouse_move_down` | Move cursor down | | +| `controlify:vmouse_move_left` | Move cursor left | | +| `controlify:vmouse_move_right` | Move cursor right | | +| `controlify:vmouse_snap_up` | Snap to nearest element up | | +| `controlify:vmouse_snap_down` | Snap to nearest element down | | +| `controlify:vmouse_snap_left` | Snap to nearest element left | | +| `controlify:vmouse_snap_right` | Snap to nearest element right | | +| `controlify:vmouse_lclick` | Left-click | | +| `controlify:vmouse_rclick` | Right-click | | +| `controlify:vmouse_shift_click` | Shift-click | | +| `controlify:vmouse_scroll_down` | Scroll down | | +| `controlify:vmouse_scroll_up` | Scroll up | | +| `controlify:vmouse_shift` | Hold Shift modifier | | +| `controlify:vmouse_page_next` | Page next | | +| `controlify:vmouse_page_prev` | Page previous | | +| `controlify:vmouse_page_down` | Page down | | +| `controlify:vmouse_page_up` | Page up | | +| `controlify:vmouse_toggle` | Toggle virtual mouse | | diff --git a/docs/resource-packs/_meta.json b/docs/resource-packs/_meta.json index 760d2aa6..88c3d21f 100644 --- a/docs/resource-packs/_meta.json +++ b/docs/resource-packs/_meta.json @@ -2,5 +2,6 @@ "custom-controller-identification.mdx": "Custom Controller Identification", "default-binds.mdx": "Default Binds", "input-glyphs.mdx": "Input Glyphs", - "guides.mdx": "Button Guides" + "guides.mdx": "Button Guides", + "keyboard-layouts.mdx": "Keyboard Layouts" } diff --git a/docs/resource-packs/keyboard-layouts.mdx b/docs/resource-packs/keyboard-layouts.mdx new file mode 100644 index 00000000..339dd518 --- /dev/null +++ b/docs/resource-packs/keyboard-layouts.mdx @@ -0,0 +1,183 @@ +--- +title: Keyboard Layouts +--- + +Design, data format, and examples for Controlify's on‑screen keyboard layouts. + +## What is it? +A keyboard layout is a data file that describes the rows and keys shown by the on‑screen keyboard. Layouts are column‑aligned grids: each row’s key widths must sum to the same total “unit width.” Keys have an optional shifted function to support Shift and Caps‑style behavior. + +Layouts are discovered from resource packs and selected per language with a fallback to en_us. + +- Location: `assets//keyboard_layout//.json` +- Resolved id: `:` +- Language fallback: if `.json` is missing, `en_us.json` is used +- Built‑ins: `controlify:full`, `controlify:simple`, `controlify:server_ip` (see KeyboardLayouts) +- Fallback: if a requested layout can’t be found/parsed, an internal QWERTY fallback is used + + +Row widths are validated. If any row’s key widths don’t add up to the layout width, the file won’t load. + + +## File format +Top level +- `width` (number ≥ 1): the unit width of the keyboard; every row must sum to this +- `keys` (array of rows): each row is an array of Key entries; rows are rendered top‑to‑bottom + +A Key can be specified in multiple forms: +- Short: a single KeyFunction (e.g. a string like "a"), meaning regular=shifted=createShifted() +- Pair: `[regular, shifted]` array +- Object: `{ regular, shifted?, width?, shortcut?, identifier? }` + +Key object fields +- `regular` (KeyFunction): action when not shifted +- `shifted` (KeyFunction, optional): action when shifted; default is `regular.createShifted()` (may return itself if not supported) +- `width` (number ≥ 0.1, default 1): unit width of this key +- `shortcut` (ResourceLocation, optional): binding id whose glyph is shown on the key and which can trigger this key directly (see [Built-in Bindings](../reference/builtin-bindings)) +- `identifier` (string, optional): stable id used to preserve focus when switching layouts + +### KeyFunction types +You can mix any of these in a row. + +#### String insert +A KeyFunction that inserts a string of characters. It can be specified in two forms: + +1. Short form: a JSON string value (e.g. `"a"`) +2. Object form: `{ chars: string, display_name: Component? }` + +Shifted behavior: by default, the shifted variant uses `toUpperCase()` of `chars` unless you provide an explicit `shifted` KeyFunction. + +#### Key codes +A KeyFunction that sends one or more key codes ([GLFW/Minecraft `InputConstants` values](https://www.glfw.org/docs/latest/group__keys.html)). +A key code is a numeric identifier for a key on a keyboard, such as `GLFW_KEY_A (65)` or `GLFW_KEY_ENTER (257)`. +They represent a physical key on your keyboard, rather than a printable character. + + +Common key codes used in keyboards can be replaced with [special actions](#special-actions) for better readability and +localisation benefits. + + +It can be specified in two forms: + +1. Short form: a JSON array of integer key codes (e.g. `[257, 259]`) +2. Object form: a JSON array of key code objects, each with: + - `keycode` (integer): the key code + - `scancode` (integer, optional, default 0): the physical scancode (if applicable) + - `modifier` (integer, optional, default 0): [modifier bitflags](https://www.glfw.org/docs/latest/group__mods.html) (e.g. `GLFW_MOD_SHIFT`) + +There is no default shifted variant for key codes; if you need a shifted version, provide it explicitly using the `shifted` KeyFunction. + +```json +{ "codes": [ + 257, + { "keycode": 259, "scancode": 0, "modifier": 0 } +]} +``` + +#### Special actions +A KeyFunction that performs a predefined action, such as pressing Enter, Shift Key, copying text, etc. + +```json +{ "action": "shift" } +``` +Available actions +- `shift`, `shift_lock` +- `enter`, `backspace`, `tab` +- `left_arrow`, `right_arrow`, `up_arrow`, `down_arrow` +- `copy_all`, `paste` +- `previous_layout` + +Display names are translatable via `controlify.keyboard.special.`, e.g. `controlify.keyboard.special.enter`. + +#### Change layout +A KeyFunction that switches to another layout by id. It can be specified as: +1. Object form: `{ layout: ResourceLocation, display_name: Component }` + +```json +{ "layout": "my_pack:emojis", "display_name": { "text": "Emoji" } } +``` + +### Display names +Where a display name is supported, use a [Minecraft Component](https://minecraft.wiki/w/Text_component_format#Java_Edition). +Examples: +```json +{ "text": "Enter" } +{ "translate": "my.lang.key" } +``` + +## Examples +Minimal keyboard +```json +{ + "width": 10, + "keys": [ + [ "q", "w", "e", "r", "t", "y", "u", "i", "o", "p" ], + [ "a", "s", "d", "f", "g", "h", "j", "k", "l", [".", ","] ], + [ "z", "x", "c", "v", "b", "n", "m", ["-", "_"], { "regular": { "action": "enter" }, "width": 2.0 } ] + ] +} +``` + +Explicit shifted with width and shortcut glyphs +```json +{ + "width": 13, + "keys": [ + [ + { "regular": { "action": "tab" }, "width": 1 }, + [ ["a", "A"] ], + { "regular": { "chars": "b" }, "shifted": { "chars": "B" }, "width": 2, "shortcut": "controlify:gui_abstract_action_1", "identifier": "big-b" }, + { "action": "enter" } + ] + ] +} +``` + +Switch to another layout and back +```json +{ + "width": 5, + "keys": [ + [ + { "layout": "my_pack:numeric", "display_name": { "text": "123" }, "identifier": "to-numeric" }, + { "action": "previous_layout" } + ] + ] +} +``` + +## Behavior notes +- Keys are column‑aligned; spanning multiple columns isn’t supported. Use `width` to vary sizes. +- Shift toggles the shifted layer for the next press; Shift‑lock keeps it engaged until toggled off. +- Paste inserts clipboard text as characters; Copy‑all calls the target’s copy handler. +- When changing layouts, focus is preserved using `identifier` if possible, or by focusing a `previous_layout` key. + + +If you only provide a single KeyFunction (e.g. a string), the key uses that for both layers, with the string’s uppercase form as the shifted display by default. + + +## Common conventions + +It's recommended to follow these conventions when creating your own keyboard layouts: + +- Backspace key (if present) should have a shortcut of `controlify:gui_abstract_action_1`. +- Space key (if present) should have a shortcut of `controlify:gui_abstract_action_2`. +- Shift key (if present) should have a shortcut of `controlify:gui_abstract_action_3`. +- Enter key (if present) should have a shortcut of `controlify:pause`. +- The user expects `controlify:gui_next_tab` and `controlify:gui_prev_tab` should not be used as shortcuts, + as the user expects them to be used to move the text cursor. +- Prefer using [special actions](#special-actions) for common keys like Enter, Shift, and Backspace, + as opposed to using key codes, for better readability and localisation. +- When localising keyboards, prefer to keep the same special actions in the same order where possible. +- When localising keyboards, prefer the most standard layout for the language, such as QWERTY for English, AZERTY for French, etc. +- It is not necessary to localise the display names, as they're already in a localised keyboard layout file. + +## JSON Schema + +Add the following to the top of your keyboard layout JSON file to get schema validation in your IDE: + +```json +{ + "$schema": "https://erebus.moddedmc.wiki/api/v1/docs/controlify/asset/controlify:schemas/keyboard_layout" +} +``` diff --git a/docs/schemas/keyboard_layout.json b/docs/schemas/keyboard_layout.json new file mode 100644 index 00000000..61e4b150 --- /dev/null +++ b/docs/schemas/keyboard_layout.json @@ -0,0 +1,141 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://controlify.dev/schemas/keyboard-layout.json", + "title": "Controlify Keyboard Layout", + "type": "object", + "additionalProperties": false, + "required": ["width", "keys"], + "properties": { + "width": { + "type": "number", + "minimum": 1, + "description": "Unit width of the layout; each row's key widths must sum to this value." + }, + "keys": { + "type": "array", + "minItems": 1, + "items": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/keyEntry" }, + "description": "Row of keys, left-to-right." + }, + "description": "Rows, rendered top-to-bottom." + } + }, + "$defs": { + "resourceLocation": { + "type": "string", + "pattern": "^[a-z0-9_.-]+:[a-z0-9/._-]+$", + "description": "Minecraft resource location (namespace:path)." + }, + "component": { + "oneOf": [ + { + "type": "object", + "description": "Minecraft text component (e.g. {\"text\":\"...\"} or {\"translate\":\"...\"}).", + "additionalProperties": true, + "minProperties": 1 + }, + { + "type": "string", + "description": "Short literal form of a text component, e.g. \"Hello World\"." + } + ] + }, + "keyCode": { + "oneOf": [ + { "type": "integer", "description": "Keycode (GLFW/Minecraft logical key)." }, + { + "type": "object", + "additionalProperties": false, + "required": ["keycode"], + "properties": { + "keycode": { "type": "integer" }, + "scancode": { "type": "integer", "default": 0 }, + "modifier": { "type": "integer", "default": 0 } + } + } + ] + }, + "keyFunction": { + "oneOf": [ + { "type": "string", "description": "String insertion (short form)." }, + { + "type": "object", + "additionalProperties": false, + "required": ["chars"], + "properties": { + "chars": { "type": "string" }, + "display_name": { "$ref": "#/$defs/component" } + }, + "description": "String insertion (object form)." + }, + { + "type": "object", + "additionalProperties": false, + "required": ["codes", "display_name"], + "properties": { + "codes": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/keyCode" } }, + "display_name": { "$ref": "#/$defs/component" } + }, + "description": "Key code sequence." + }, + { + "type": "object", + "additionalProperties": false, + "required": ["action"], + "properties": { + "action": { + "type": "string", + "enum": [ + "shift", "shift_lock", + "enter", "backspace", "tab", + "left_arrow", "right_arrow", "up_arrow", "down_arrow", + "copy_all", "paste", + "previous_layout" + ] + } + }, + "description": "Special action key." + }, + { + "type": "object", + "additionalProperties": false, + "required": ["layout", "display_name"], + "properties": { + "layout": { "$ref": "#/$defs/resourceLocation" }, + "display_name": { "$ref": "#/$defs/component" } + }, + "description": "Change layout key." + } + ] + }, + "keyObject": { + "type": "object", + "additionalProperties": false, + "required": ["regular"], + "properties": { + "regular": { "$ref": "#/$defs/keyFunction" }, + "shifted": { "$ref": "#/$defs/keyFunction" }, + "width": { "type": "number", "minimum": 0.1, "default": 1 }, + "shortcut": { "$ref": "#/$defs/resourceLocation" }, + "identifier": { "type": "string" } + }, + "description": "Full key definition with optional shifted function, width, shortcut glyph binding, and focus identifier." + }, + "keyEntry": { + "oneOf": [ + { "$ref": "#/$defs/keyFunction" }, + { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { "$ref": "#/$defs/keyFunction" }, { "$ref": "#/$defs/keyFunction" } ], + "description": "[regular, shifted] pair" + }, + { "$ref": "#/$defs/keyObject" } + ] + } + } +} diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 30dafb7f..b6137ad4 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -4,7 +4,6 @@ import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.api.bind.ControlifyBindApi; import dev.isxander.controlify.api.entrypoint.InitContext; -import dev.isxander.controlify.api.entrypoint.PreInitContext; import dev.isxander.controlify.api.guide.ContainerCtx; import dev.isxander.controlify.api.guide.GuideDomainRegistries; import dev.isxander.controlify.api.guide.GuideDomainRegistry; @@ -36,7 +35,7 @@ import dev.isxander.controlify.platform.main.PlatformMainUtil; import dev.isxander.controlify.platform.network.SidedNetworkApi; import dev.isxander.controlify.rumble.RumbleManager; -import dev.isxander.controlify.screenkeyboard.KeyboardLayoutManager; +import dev.isxander.controlify.screenop.keyboard.KeyboardLayoutManager; import dev.isxander.controlify.server.*; import dev.isxander.controlify.screenop.ScreenProcessorProvider; import dev.isxander.controlify.config.ControlifyConfig; diff --git a/src/main/java/dev/isxander/controlify/compatibility/yacl/mixins/StringControllerElementAccessor.java b/src/main/java/dev/isxander/controlify/compatibility/yacl/mixins/StringControllerElementAccessor.java new file mode 100644 index 00000000..67ddd2cb --- /dev/null +++ b/src/main/java/dev/isxander/controlify/compatibility/yacl/mixins/StringControllerElementAccessor.java @@ -0,0 +1,11 @@ +package dev.isxander.controlify.compatibility.yacl.mixins; + +import dev.isxander.yacl3.gui.controllers.string.StringControllerElement; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(StringControllerElement.class) +public interface StringControllerElementAccessor { + @Invoker + boolean callDoCopy(); +} diff --git a/src/main/java/dev/isxander/controlify/compatibility/yacl/mixins/StringControllerElementMixin.java b/src/main/java/dev/isxander/controlify/compatibility/yacl/mixins/StringControllerElementMixin.java new file mode 100644 index 00000000..ed1be000 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/compatibility/yacl/mixins/StringControllerElementMixin.java @@ -0,0 +1,21 @@ +package dev.isxander.controlify.compatibility.yacl.mixins; + +import dev.isxander.controlify.compatibility.yacl.screenop.StringControllerElementComponentProcessor; +import dev.isxander.controlify.screenop.ComponentProcessor; +import dev.isxander.controlify.screenop.ComponentProcessorProvider; +import dev.isxander.yacl3.gui.controllers.string.StringControllerElement; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(StringControllerElement.class) +public class StringControllerElementMixin implements ComponentProcessorProvider { + @Unique + private final ComponentProcessor componentProcessor = new StringControllerElementComponentProcessor( + (StringControllerElement) (Object) this + ); + + @Override + public ComponentProcessor componentProcessor() { + return componentProcessor; + } +} diff --git a/src/main/java/dev/isxander/controlify/compatibility/yacl/screenop/StringControllerElementComponentProcessor.java b/src/main/java/dev/isxander/controlify/compatibility/yacl/screenop/StringControllerElementComponentProcessor.java new file mode 100644 index 00000000..ae7ce4d5 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/compatibility/yacl/screenop/StringControllerElementComponentProcessor.java @@ -0,0 +1,79 @@ +package dev.isxander.controlify.compatibility.yacl.screenop; + +import com.mojang.blaze3d.platform.InputConstants; +import dev.isxander.controlify.compatibility.yacl.mixins.StringControllerElementAccessor; +import dev.isxander.controlify.controller.ControllerEntity; +import dev.isxander.controlify.screenop.ComponentProcessor; +import dev.isxander.controlify.screenop.ScreenProcessor; +import dev.isxander.controlify.screenop.keyboard.ComponentKeyboardBehaviour; +import dev.isxander.controlify.screenop.keyboard.InputTarget; +import dev.isxander.controlify.screenop.keyboard.KeyboardLayouts; +import dev.isxander.controlify.screenop.keyboard.KeyboardOverlayScreen; +import dev.isxander.yacl3.gui.controllers.string.StringControllerElement; + +public class StringControllerElementComponentProcessor implements ComponentProcessor { + private final StringControllerElement element; + + public StringControllerElementComponentProcessor(StringControllerElement element) { + this.element = element; + } + + @Override + public ComponentKeyboardBehaviour getKeyboardBehaviour(ScreenProcessor screen, ControllerEntity controller) { + return new ComponentKeyboardBehaviour.Handled( + KeyboardLayouts.simple(), + new StringInputTarget(), + KeyboardOverlayScreen.aboveOrBelowWidgetPositioner( + (int) (screen.screen.width * 0.8f), (int) (screen.screen.height * 0.4f), + 1, + element::getRectangle + ) + ); + } + + private class StringInputTarget implements InputTarget { + @Override + public boolean supportsCharInput() { + return true; + } + + @Override + public boolean acceptChar(char ch, int modifiers) { + return element.charTyped(ch, modifiers); + } + + @Override + public boolean supportsKeyCodeInput() { + return true; + } + + @Override + public boolean acceptKeyCode(int keycode, int scancode, int modifiers) { + return element.keyPressed(keycode, scancode, modifiers); + } + + @Override + public boolean supportsCursorMovement() { + return true; + } + + @Override + public boolean moveCursor(int amount) { + int keycode = amount > 0 ? InputConstants.KEY_RIGHT : InputConstants.KEY_LEFT; + for (int i = 0; i < amount; i++) { + element.keyPressed(keycode, 0, 0); + } + return true; + } + + @Override + public boolean supportsCopying() { + return true; + } + + @Override + public boolean copy() { + return ((StringControllerElementAccessor) element).callDoCopy(); + } + } +} diff --git a/src/main/java/dev/isxander/controlify/controller/GenericControllerConfig.java b/src/main/java/dev/isxander/controlify/controller/GenericControllerConfig.java index e4f26d58..5e6d0e33 100644 --- a/src/main/java/dev/isxander/controlify/controller/GenericControllerConfig.java +++ b/src/main/java/dev/isxander/controlify/controller/GenericControllerConfig.java @@ -26,6 +26,7 @@ public class GenericControllerConfig implements ConfigClass { public boolean dontShowControllerSubmission = false; - public boolean hintChatCursorMovement = true; - public boolean hintCommandSuggester = true; + public boolean hintKeyboardCursor = true; + public boolean hintKeyboardCommandSuggester = true; + public boolean hintKeyboardSignLine = true; } diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/BookEditScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/patches/bookfocusfix/BookEditScreenMixin.java similarity index 94% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/BookEditScreenMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/patches/bookfocusfix/BookEditScreenMixin.java index a02ab77c..b0ab58ec 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/BookEditScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/patches/bookfocusfix/BookEditScreenMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.patches.bookfocusfix; import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; import dev.isxander.controlify.Controlify; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/editbox/EditBoxMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/editbox/EditBoxMixin.java deleted file mode 100644 index e8b6a6a5..00000000 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/editbox/EditBoxMixin.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.isxander.controlify.mixins.feature.screenkeyboard.editbox; - -import dev.isxander.controlify.screenkeyboard.KeyboardSupportedMarker; -import net.minecraft.client.gui.components.EditBox; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; - -@Mixin(EditBox.class) -public class EditBoxMixin implements KeyboardSupportedMarker.Mutable { - - @Unique - private boolean keyboardSupported = false; - - @Override - public void controlify$setKeyboardSupported(boolean supported) { - this.keyboardSupported = supported; - } - - @Override - public boolean controlify$isKeyboardSupported() { - return this.keyboardSupported; - } -} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/MinecraftMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/MinecraftMixin.java index 6b772386..21202360 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/MinecraftMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/MinecraftMixin.java @@ -1,7 +1,10 @@ package dev.isxander.controlify.mixins.feature.screenop; +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.llamalad7.mixinextras.sugar.Local; import dev.isxander.controlify.screenop.ComponentProcessorProvider; import dev.isxander.controlify.screenop.ScreenProcessorProvider; +import dev.isxander.controlify.screenop.keyboard.KeyboardOverlayScreen; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.Screen; import org.spongepowered.asm.mixin.Mixin; @@ -15,4 +18,9 @@ public class MinecraftMixin { private void changeScreen(Screen screen, CallbackInfo ci) { ComponentProcessorProvider.REGISTRY.clearCache(); } + + @WrapWithCondition(method = "setScreen", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/Screen;removed()V")) + private boolean preventRemovingOldScreen(Screen oldScreen, @Local(argsOnly = true) Screen newScreen) { + return !(newScreen instanceof KeyboardOverlayScreen); + } } diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/bundle/BundleMouseActionsMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/bundle/BundleMouseActionsMixin.java similarity index 87% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/bundle/BundleMouseActionsMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/bundle/BundleMouseActionsMixin.java index 4d673817..07bb8861 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/bundle/BundleMouseActionsMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/bundle/BundleMouseActionsMixin.java @@ -1,6 +1,5 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla.bundle; +package dev.isxander.controlify.mixins.feature.screenop.impl.bundle; -import net.minecraft.client.Minecraft; import org.spongepowered.asm.mixin.Mixin; //? if >=1.21.2 { @@ -22,6 +21,8 @@ public abstract class BundleMouseActionsMixin implements ItemSlotControllerActio } } //?} else { -/*@Mixin(Minecraft.class) +/*import net.minecraft.client.Minecraft; + +@Mixin(Minecraft.class) public class BundleMouseActionsMixin {} *///?} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/ChatComponentMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/chat/ChatComponentMixin.java similarity index 94% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/ChatComponentMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/chat/ChatComponentMixin.java index b0f7cb81..adc389f2 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/ChatComponentMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/chat/ChatComponentMixin.java @@ -1,9 +1,9 @@ -package dev.isxander.controlify.mixins.feature.screenkeyboard.chat; +package dev.isxander.controlify.mixins.feature.screenop.impl.chat; import com.llamalad7.mixinextras.expression.Definition; import com.llamalad7.mixinextras.expression.Expression; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; -import dev.isxander.controlify.screenkeyboard.ChatKeyboardDucky; +import dev.isxander.controlify.screenop.keyboard.ChatKeyboardDucky; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.ChatComponent; import net.minecraft.client.gui.screens.ChatScreen; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/ChatScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/chat/ChatScreenMixin.java similarity index 90% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/ChatScreenMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/chat/ChatScreenMixin.java index 68aaa4cc..2d8ebfad 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/ChatScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/chat/ChatScreenMixin.java @@ -1,13 +1,13 @@ -package dev.isxander.controlify.mixins.feature.screenkeyboard.chat; +package dev.isxander.controlify.mixins.feature.screenop.impl.chat; import com.llamalad7.mixinextras.expression.Definition; import com.llamalad7.mixinextras.expression.Expression; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.mojang.blaze3d.platform.InputConstants; import dev.isxander.controlify.api.ControlifyApi; -import dev.isxander.controlify.screenkeyboard.*; import dev.isxander.controlify.screenop.ScreenProcessorProvider; import dev.isxander.controlify.screenop.compat.vanilla.ChatScreenProcessor; +import dev.isxander.controlify.screenop.keyboard.*; import net.minecraft.client.gui.components.CommandSuggestions; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.events.GuiEventListener; @@ -25,15 +25,15 @@ import java.util.List; @Mixin(ChatScreen.class) -public abstract class ChatScreenMixin extends Screen implements ScreenProcessorProvider, MixinInputTarget, ChatKeyboardDucky, KeyboardSupportedMarker { - @Unique private KeyboardWidget keyboard; - @Unique private float shiftChatAmt = 0f; +public abstract class ChatScreenMixin extends Screen implements ScreenProcessorProvider, MixinInputTarget, ChatKeyboardDucky { + @Shadow protected EditBox input; @Shadow CommandSuggestions commandSuggestions; @Shadow public abstract boolean keyPressed(int keyCode, int scanCode, int modifiers); - @Unique - private final ChatScreenProcessor screenProcessor = new ChatScreenProcessor( + @Unique private KeyboardWidget keyboard; + @Unique private float shiftChatAmt = 0f; + @Unique private final ChatScreenProcessor screenProcessor = new ChatScreenProcessor( (ChatScreen) (Object) this, () -> this.input, () -> this.keyboard, @@ -56,15 +56,10 @@ private void addKeyboard(CallbackInfo ci) { this.shiftChatAmt = 0.5f; int keyboardHeight = (int) (this.height * this.shiftChatAmt); - this.addRenderableWidget(this.keyboard = new KeyboardWidget(0, this.height - keyboardHeight, this.width, keyboardHeight, KeyboardLayouts.chat(), this, (ChatScreen) (Object) this)); + this.addRenderableWidget(this.keyboard = new KeyboardWidget(0, this.height - keyboardHeight, this.width, keyboardHeight, KeyboardLayouts.full(), this)); }); } - @Inject(method = "init", at = @At("RETURN")) - private void markEditBoxAsSupported(CallbackInfo ci) { - KeyboardSupportedMarker.setKeyboardSupported(this.input, true); - } - @ModifyArg(method = "setInitialFocus", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/ChatScreen;setInitialFocus(Lnet/minecraft/client/gui/components/events/GuiEventListener;)V")) private GuiEventListener modifyInitialFocus(GuiEventListener editBox) { return this.keyboard != null ? this.keyboard : editBox; @@ -109,8 +104,6 @@ private int modifyMaxSuggestionCount(int count) { return this.shiftChatAmt; } - - @Override public boolean controlify$supportsCharInput() { return true; @@ -152,12 +145,19 @@ private int modifyMaxSuggestionCount(int count) { } @Override - public ChatScreenProcessor screenProcessor() { - return this.screenProcessor; + public boolean controlify$supportsCursorMovement() { + return true; } @Override - public boolean controlify$isKeyboardSupported() { - return this.keyboard != null; + public boolean controlify$moveCursor(int amount) { + this.input.moveCursor(amount, false); + return true; } + + @Override + public ChatScreenProcessor screenProcessor() { + return this.screenProcessor; + } + } diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/CommandSuggestionsMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/chat/CommandSuggestionsMixin.java similarity index 92% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/CommandSuggestionsMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/chat/CommandSuggestionsMixin.java index b4e9592b..198991e2 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/chat/CommandSuggestionsMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/chat/CommandSuggestionsMixin.java @@ -1,7 +1,7 @@ -package dev.isxander.controlify.mixins.feature.screenkeyboard.chat; +package dev.isxander.controlify.mixins.feature.screenop.impl.chat; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; -import dev.isxander.controlify.screenkeyboard.ChatKeyboardDucky; +import dev.isxander.controlify.screenop.keyboard.ChatKeyboardDucky; import dev.isxander.controlify.screenop.compat.vanilla.ChatScreenProcessor; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.CommandSuggestions; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractContainerScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/container/AbstractContainerScreenMixin.java similarity index 98% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractContainerScreenMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/container/AbstractContainerScreenMixin.java index b98836d9..b1ac707c 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractContainerScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/container/AbstractContainerScreenMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.container; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.sugar.Share; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractRecipeBookScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/container/AbstractRecipeBookScreenMixin.java similarity index 96% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractRecipeBookScreenMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/container/AbstractRecipeBookScreenMixin.java index 77098b6e..0a8ef774 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractRecipeBookScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/container/AbstractRecipeBookScreenMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.container; import dev.isxander.controlify.screenop.ScreenProcessor; import dev.isxander.controlify.screenop.ScreenProcessorProvider; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/CreativeModeInventoryScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/container/CreativeModeInventoryScreenMixin.java similarity index 80% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/CreativeModeInventoryScreenMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/container/CreativeModeInventoryScreenMixin.java index 422d1c93..8e91dbc8 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/CreativeModeInventoryScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/container/CreativeModeInventoryScreenMixin.java @@ -1,12 +1,9 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.container; import dev.isxander.controlify.screenop.ScreenProcessor; import dev.isxander.controlify.screenop.ScreenProcessorProvider; import dev.isxander.controlify.screenop.compat.vanilla.CreativeModeInventoryScreenProcessor; -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen; -import net.minecraft.network.chat.Component; -import net.minecraft.world.entity.player.Inventory; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/MerchantScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/container/MerchantScreenMixin.java similarity index 97% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/MerchantScreenMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/container/MerchantScreenMixin.java index 0754e0ee..bb721e04 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/MerchantScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/container/MerchantScreenMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.container; import com.llamalad7.mixinextras.expression.Definition; import com.llamalad7.mixinextras.expression.Expression; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractButtonMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/AbstractButtonMixin.java similarity index 91% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractButtonMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/AbstractButtonMixin.java index 9e509c7f..6d4e1e6f 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractButtonMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/AbstractButtonMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.elements; import dev.isxander.controlify.screenop.ComponentProcessor; import dev.isxander.controlify.screenop.ComponentProcessorProvider; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractContainerEventHandlerMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/AbstractContainerEventHandlerMixin.java similarity index 89% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractContainerEventHandlerMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/AbstractContainerEventHandlerMixin.java index ccc9634a..77a793ec 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractContainerEventHandlerMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/AbstractContainerEventHandlerMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.elements; import dev.isxander.controlify.screenop.CustomFocus; import net.minecraft.client.gui.components.events.AbstractContainerEventHandler; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractSelectionListMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/AbstractSelectionListMixin.java similarity index 85% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractSelectionListMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/AbstractSelectionListMixin.java index 9225c3b3..f5aef8d3 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractSelectionListMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/AbstractSelectionListMixin.java @@ -1,8 +1,7 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.elements; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import dev.isxander.controlify.Controlify; -import dev.isxander.controlify.InputMode; import net.minecraft.client.gui.components.AbstractSelectionList; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractSliderButtonMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/AbstractSliderButtonMixin.java similarity index 95% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractSliderButtonMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/AbstractSliderButtonMixin.java index 3621f706..9db212b6 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractSliderButtonMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/AbstractSliderButtonMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.elements; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import dev.isxander.controlify.Controlify; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/ContainerObjectSelectionListEntryMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/ContainerObjectSelectionListEntryMixin.java similarity index 89% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/ContainerObjectSelectionListEntryMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/ContainerObjectSelectionListEntryMixin.java index e85d437c..10804a01 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/ContainerObjectSelectionListEntryMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/ContainerObjectSelectionListEntryMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.elements; import dev.isxander.controlify.screenop.CustomFocus; import net.minecraft.client.gui.components.ContainerObjectSelectionList; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/EditBoxMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/EditBoxMixin.java new file mode 100644 index 00000000..228cd470 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/elements/EditBoxMixin.java @@ -0,0 +1,105 @@ +package dev.isxander.controlify.mixins.feature.screenop.impl.elements; + +import com.llamalad7.mixinextras.expression.Definition; +import com.llamalad7.mixinextras.expression.Expression; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef; +import dev.isxander.controlify.api.ControlifyApi; +import dev.isxander.controlify.bindings.ControlifyBindings; +import dev.isxander.controlify.font.BindingFontHelper; +import dev.isxander.controlify.screenop.ComponentProcessor; +import dev.isxander.controlify.screenop.ComponentProcessorProvider; +import dev.isxander.controlify.screenop.compat.vanilla.EditBoxComponentProcessor; +import dev.isxander.controlify.screenop.keyboard.ComponentKeyboardBehaviour; +import dev.isxander.controlify.screenop.keyboard.KeyboardOverlayScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(EditBox.class) +public abstract class EditBoxMixin extends AbstractWidget implements ComponentProcessorProvider { + + @Shadow private @Nullable Component hint; + @Shadow private @Nullable String suggestion; + @Shadow private int textX; + @Shadow private int textY; + @Shadow @Final private Font font; + + @Unique private final EditBoxComponentProcessor processor = new EditBoxComponentProcessor( + (EditBox) (Object) this, + Minecraft.getInstance().getWindow().getGuiScaledWidth(), + Minecraft.getInstance().getWindow().getGuiScaledHeight() + ); + @Unique private static final Component HINT_TEXT = Component.translatable( + "controlify.hint.edit_box_keyboard", + BindingFontHelper.binding(ControlifyBindings.GUI_PRESS) + ); + + public EditBoxMixin(int x, int y, int width, int height, Component message) { + super(x, y, width, height, message); + } + + /** + * Renders some hint text when the edit box is focused to indicate + * that pressing GUI_PRESS will open the on-screen keyboard. + * If the edit box has some text, the hint will be minimally rendered + */ + @ModifyExpressionValue(method = "renderWidget", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/Font;plainSubstrByWidth(Ljava/lang/String;I)Ljava/lang/String;")) + private String renderHintText(String renderedValue, @Local(argsOnly = true) GuiGraphics graphics, @Share("renderHint") LocalBooleanRef renderHint) { + renderHint.set(false); + + ControlifyApi.get().getCurrentController().ifPresent(controller -> { + if (this.isFocused() + && controller.genericConfig().config().showOnScreenKeyboard + && ControlifyApi.get().currentInputMode().isController() + && !(Minecraft.getInstance().screen instanceof KeyboardOverlayScreen) + && processor.getKeyboardBehaviour() instanceof ComponentKeyboardBehaviour.Handled + ) { + if (renderedValue.isEmpty() + && this.hint == null + && this.suggestion == null + ) { + renderHint.set(true); + graphics.drawString(font, HINT_TEXT, this.textX, this.textY, 0xFFAAAAAA); + } else { + var component = BindingFontHelper.binding(ControlifyBindings.GUI_PRESS); + int width = font.width(component); + + graphics.drawString( + font, component, + this.getX() - 2 - width, + this.textY, + -1 + ); + } + } + }); + + return renderedValue; + } + + @Definition(id = "isFocused", method = "Lnet/minecraft/client/gui/components/EditBox;isFocused()Z") + @Definition(id = "getMillis", method = "Lnet/minecraft/Util;getMillis()J") + @Definition(id = "focusedTime", field = "Lnet/minecraft/client/gui/components/EditBox;focusedTime:J") + @Expression("(getMillis() - this.focusedTime) / 300 % 2 == 0") + @ModifyExpressionValue(method = "renderWidget", at = @At("MIXINEXTRAS:EXPRESSION")) + private boolean preventShowingCursor(boolean showCursor, @Share("renderHint") LocalBooleanRef renderHint) { + return showCursor && !renderHint.get(); + } + + @Override + public ComponentProcessor componentProcessor() { + return processor; + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/CreateWorldScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/CreateWorldScreenMixin.java similarity index 97% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/CreateWorldScreenMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/CreateWorldScreenMixin.java index 7edcbac6..7e8e83f9 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/CreateWorldScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/CreateWorldScreenMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import dev.isxander.controlify.api.buttonguide.ButtonGuideApi; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/DirectJoinServerScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/DirectJoinServerScreenMixin.java new file mode 100644 index 00000000..95c66261 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/DirectJoinServerScreenMixin.java @@ -0,0 +1,48 @@ +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; + +import com.llamalad7.mixinextras.expression.Definition; +import com.llamalad7.mixinextras.expression.Expression; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import dev.isxander.controlify.screenop.ScreenProcessor; +import dev.isxander.controlify.screenop.ScreenProcessorProvider; +import dev.isxander.controlify.screenop.compat.vanilla.AddServerLikeScreenProcessor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.DirectJoinServerScreen; +import net.minecraft.client.gui.screens.Screen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(DirectJoinServerScreen.class) +public class DirectJoinServerScreenMixin implements ScreenProcessorProvider { + @Shadow + private EditBox ipEdit; + @Shadow + private Button selectButton; + @Unique + private Button cancelButton; + + @Unique + private final AddServerLikeScreenProcessor screenProcessor = new AddServerLikeScreenProcessor( + (Screen) (Object) this, + () -> this.ipEdit, + () -> this.selectButton, + () -> this.cancelButton + ); + + @Definition(id = "builder", method = "Lnet/minecraft/client/gui/components/Button;builder(Lnet/minecraft/network/chat/Component;Lnet/minecraft/client/gui/components/Button$OnPress;)Lnet/minecraft/client/gui/components/Button$Builder;") + @Definition(id = "GUI_CANCEL", field = "Lnet/minecraft/network/chat/CommonComponents;GUI_CANCEL:Lnet/minecraft/network/chat/Component;") + @Definition(id = "build", method = "Lnet/minecraft/client/gui/components/Button$Builder;build()Lnet/minecraft/client/gui/components/Button;") + @Expression("builder(GUI_CANCEL, ?).?(?, ?, ?, ?).build()") + @ModifyExpressionValue(method = "init", at = @At("MIXINEXTRAS:EXPRESSION")) + private Button captureCancelButton(Button button) { + return this.cancelButton = button; + } + + @Override + public ScreenProcessor screenProcessor() { + return this.screenProcessor; + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/EditServerScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/EditServerScreenMixin.java new file mode 100644 index 00000000..1ea2012a --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/EditServerScreenMixin.java @@ -0,0 +1,48 @@ +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; + +import com.llamalad7.mixinextras.expression.Definition; +import com.llamalad7.mixinextras.expression.Expression; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import dev.isxander.controlify.screenop.ScreenProcessor; +import dev.isxander.controlify.screenop.ScreenProcessorProvider; +import dev.isxander.controlify.screenop.compat.vanilla.AddServerLikeScreenProcessor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.EditServerScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(EditServerScreen.class) +public class EditServerScreenMixin implements ScreenProcessorProvider { + + @Shadow + private EditBox ipEdit; + @Shadow + private Button addButton; + @Unique + private Button cancelButton; + + @Unique + private final AddServerLikeScreenProcessor screenProcessor = new AddServerLikeScreenProcessor( + (EditServerScreen) (Object) this, + () -> this.ipEdit, + () -> this.addButton, + () -> this.cancelButton + ); + + @Definition(id = "builder", method = "Lnet/minecraft/client/gui/components/Button;builder(Lnet/minecraft/network/chat/Component;Lnet/minecraft/client/gui/components/Button$OnPress;)Lnet/minecraft/client/gui/components/Button$Builder;") + @Definition(id = "GUI_CANCEL", field = "Lnet/minecraft/network/chat/CommonComponents;GUI_CANCEL:Lnet/minecraft/network/chat/Component;") + @Definition(id = "build", method = "Lnet/minecraft/client/gui/components/Button$Builder;build()Lnet/minecraft/client/gui/components/Button;") + @Expression("builder(GUI_CANCEL, ?).?(?, ?, ?, ?).build()") + @ModifyExpressionValue(method = "init", at = @At("MIXINEXTRAS:EXPRESSION")) + private Button captureCancelButton(Button button) { + return this.cancelButton = button; + } + + @Override + public ScreenProcessor screenProcessor() { + return screenProcessor; + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/JoinMultiplayerScreenAccessor.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/JoinMultiplayerScreenAccessor.java similarity index 82% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/JoinMultiplayerScreenAccessor.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/JoinMultiplayerScreenAccessor.java index 08f9b0f5..6f02c53c 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/JoinMultiplayerScreenAccessor.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/JoinMultiplayerScreenAccessor.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/JoinMultiplayerScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/JoinMultiplayerScreenMixin.java new file mode 100644 index 00000000..d56d0362 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/JoinMultiplayerScreenMixin.java @@ -0,0 +1,68 @@ +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; + +import com.llamalad7.mixinextras.expression.Definition; +import com.llamalad7.mixinextras.expression.Expression; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import dev.isxander.controlify.screenop.ScreenProcessor; +import dev.isxander.controlify.screenop.ScreenProcessorProvider; +import dev.isxander.controlify.screenop.compat.vanilla.JoinMultiplayerScreenProcessor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen; +import net.minecraft.client.gui.screens.multiplayer.ServerSelectionList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(JoinMultiplayerScreen.class) +public class JoinMultiplayerScreenMixin implements ScreenProcessorProvider { + @Shadow protected ServerSelectionList serverSelectionList; + + @Unique + private Button backButton; + @Unique + private Button directConnectButton; + @Unique + private Button addServerButton; + + @Unique + private final JoinMultiplayerScreenProcessor processor = new JoinMultiplayerScreenProcessor( + (JoinMultiplayerScreen) (Object) this, + () -> this.serverSelectionList, + () -> this.backButton, + () -> this.directConnectButton, + () -> this.addServerButton + ); + + @Definition(id = "builder", method = "Lnet/minecraft/client/gui/components/Button;builder(Lnet/minecraft/network/chat/Component;Lnet/minecraft/client/gui/components/Button$OnPress;)Lnet/minecraft/client/gui/components/Button$Builder;") + @Definition(id = "build", method = "Lnet/minecraft/client/gui/components/Button$Builder;build()Lnet/minecraft/client/gui/components/Button;") + @Definition(id = "GUI_BACK", field = "Lnet/minecraft/network/chat/CommonComponents;GUI_BACK:Lnet/minecraft/network/chat/Component;") + @Expression("builder(GUI_BACK, ?).?(?).build()") + @ModifyExpressionValue(method = "init", at = @At("MIXINEXTRAS:EXPRESSION")) + private Button captureBackButton(Button button) { + return this.backButton = button; + } + + @Definition(id = "builder", method = "Lnet/minecraft/client/gui/components/Button;builder(Lnet/minecraft/network/chat/Component;Lnet/minecraft/client/gui/components/Button$OnPress;)Lnet/minecraft/client/gui/components/Button$Builder;") + @Definition(id = "translatable", method = "Lnet/minecraft/network/chat/Component;translatable(Ljava/lang/String;)Lnet/minecraft/network/chat/MutableComponent;") + @Definition(id = "build", method = "Lnet/minecraft/client/gui/components/Button$Builder;build()Lnet/minecraft/client/gui/components/Button;") + @Expression("builder(translatable('selectServer.direct'), ?).?(?).build()") + @ModifyExpressionValue(method = "init", at = @At("MIXINEXTRAS:EXPRESSION")) + private Button captureDirectConnectButton(Button button) { + return this.directConnectButton = button; + } + + @Definition(id = "builder", method = "Lnet/minecraft/client/gui/components/Button;builder(Lnet/minecraft/network/chat/Component;Lnet/minecraft/client/gui/components/Button$OnPress;)Lnet/minecraft/client/gui/components/Button$Builder;") + @Definition(id = "translatable", method = "Lnet/minecraft/network/chat/Component;translatable(Ljava/lang/String;)Lnet/minecraft/network/chat/MutableComponent;") + @Definition(id = "build", method = "Lnet/minecraft/client/gui/components/Button$Builder;build()Lnet/minecraft/client/gui/components/Button;") + @Expression("builder(translatable('selectServer.add'), ?).?(?).build()") + @ModifyExpressionValue(method = "init", at = @At("MIXINEXTRAS:EXPRESSION")) + private Button captureAddServerButton(Button button) { + return this.addServerButton = button; + } + + @Override + public ScreenProcessor screenProcessor() { + return processor; + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/LanguageSelectionListEntryMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/LanguageSelectionListEntryMixin.java similarity index 93% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/LanguageSelectionListEntryMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/LanguageSelectionListEntryMixin.java index 47fae08d..8375ac03 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/LanguageSelectionListEntryMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/LanguageSelectionListEntryMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; import dev.isxander.controlify.screenop.ComponentProcessor; import dev.isxander.controlify.screenop.ComponentProcessorProvider; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/OptionsSubScreenAccessor.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/OptionsSubScreenAccessor.java similarity index 81% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/OptionsSubScreenAccessor.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/OptionsSubScreenAccessor.java index 7a912a0c..78359c70 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/OptionsSubScreenAccessor.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/OptionsSubScreenAccessor.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; import net.minecraft.client.gui.screens.options.OptionsSubScreen; import net.minecraft.client.gui.screens.Screen; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/PauseScreenAccessor.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/PauseScreenAccessor.java similarity index 78% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/PauseScreenAccessor.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/PauseScreenAccessor.java index 7b44d078..1dabd05e 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/PauseScreenAccessor.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/PauseScreenAccessor.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; import net.minecraft.client.gui.screens.PauseScreen; import org.spongepowered.asm.mixin.Mixin; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/PauseScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/PauseScreenMixin.java similarity index 89% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/PauseScreenMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/PauseScreenMixin.java index e733cba0..29e1a1a1 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/PauseScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/PauseScreenMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; import dev.isxander.controlify.screenop.ScreenProcessor; import dev.isxander.controlify.screenop.ScreenProcessorProvider; @@ -6,7 +6,6 @@ import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.screens.PauseScreen; import org.jetbrains.annotations.Nullable; -import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/SelectWorldScreenAccessor.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/SelectWorldScreenAccessor.java similarity index 86% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/SelectWorldScreenAccessor.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/SelectWorldScreenAccessor.java index a1ffe563..9e934e44 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/SelectWorldScreenAccessor.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/SelectWorldScreenAccessor.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/SelectWorldScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/SelectWorldScreenMixin.java similarity index 97% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/SelectWorldScreenMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/SelectWorldScreenMixin.java index bec3e877..8d1060e3 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/SelectWorldScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/SelectWorldScreenMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; import com.llamalad7.mixinextras.expression.Definition; import com.llamalad7.mixinextras.expression.Expression; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/ServerSelectionListEntryMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/ServerSelectionListEntryMixin.java similarity index 92% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/ServerSelectionListEntryMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/ServerSelectionListEntryMixin.java index 2b22986a..22faa197 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/ServerSelectionListEntryMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/ServerSelectionListEntryMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; import dev.isxander.controlify.screenop.ComponentProcessor; import dev.isxander.controlify.screenop.ComponentProcessorProvider; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/TabNavigationBarAccessor.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/TabNavigationBarAccessor.java similarity index 87% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/TabNavigationBarAccessor.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/TabNavigationBarAccessor.java index e877c1b8..efd7c619 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/TabNavigationBarAccessor.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/TabNavigationBarAccessor.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; import com.google.common.collect.ImmutableList; import net.minecraft.client.gui.components.tabs.Tab; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/TitleScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/TitleScreenMixin.java similarity index 90% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/TitleScreenMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/TitleScreenMixin.java index fa2fea23..4b5c32ab 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/TitleScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/TitleScreenMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; import dev.isxander.controlify.screenop.ScreenProcessor; import dev.isxander.controlify.screenop.ScreenProcessorProvider; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/WorldSelectionListEntryMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/WorldSelectionListEntryMixin.java similarity index 91% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/WorldSelectionListEntryMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/WorldSelectionListEntryMixin.java index 5cbdb27e..c7e4784a 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/WorldSelectionListEntryMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/outofgame/WorldSelectionListEntryMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; +package dev.isxander.controlify.mixins.feature.screenop.impl.outofgame; import dev.isxander.controlify.screenop.ComponentProcessor; import dev.isxander.controlify.screenop.ComponentProcessorProvider; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/sign/AbstractSignEditScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/sign/AbstractSignEditScreenMixin.java similarity index 61% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/sign/AbstractSignEditScreenMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/sign/AbstractSignEditScreenMixin.java index 689cca9a..5e5af6cb 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/sign/AbstractSignEditScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/sign/AbstractSignEditScreenMixin.java @@ -1,12 +1,14 @@ -package dev.isxander.controlify.mixins.feature.screenkeyboard.sign; +package dev.isxander.controlify.mixins.feature.screenop.impl.sign; import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; import com.mojang.blaze3d.platform.InputConstants; import dev.isxander.controlify.api.ControlifyApi; -import dev.isxander.controlify.screenkeyboard.KeyboardLayouts; -import dev.isxander.controlify.screenkeyboard.KeyboardSupportedMarker; -import dev.isxander.controlify.screenkeyboard.KeyboardWidget; -import dev.isxander.controlify.screenkeyboard.MixinInputTarget; +import dev.isxander.controlify.screenop.ScreenProcessor; +import dev.isxander.controlify.screenop.ScreenProcessorProvider; +import dev.isxander.controlify.screenop.compat.vanilla.AbstractSignEditScreenProcessor; +import dev.isxander.controlify.screenop.keyboard.KeyboardLayouts; +import dev.isxander.controlify.screenop.keyboard.KeyboardWidget; +import dev.isxander.controlify.screenop.keyboard.MixinInputTarget; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.font.TextFieldHelper; import net.minecraft.client.gui.screens.Screen; @@ -22,18 +24,22 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(AbstractSignEditScreen.class) -public abstract class AbstractSignEditScreenMixin extends Screen implements MixinInputTarget, KeyboardSupportedMarker { +public abstract class AbstractSignEditScreenMixin extends Screen implements ScreenProcessorProvider, MixinInputTarget { - @Shadow - private @Nullable TextFieldHelper signField; - @Shadow - @Final - private String[] messages; - @Shadow - private int line; + @Shadow private @Nullable TextFieldHelper signField; + @Shadow @Final private String[] messages; + @Shadow private int line; + @Shadow protected abstract void onDone(); - @Shadow - protected abstract void onDone(); + + @Unique + private final AbstractSignEditScreenProcessor screenProcessor = new AbstractSignEditScreenProcessor( + (AbstractSignEditScreen) (Object) this, + direction -> { + this.line = this.line + direction & 3; + this.signField.setCursorToEnd(); + } + ); @Unique private KeyboardWidget keyboard; @@ -51,11 +57,17 @@ private void addKeyboard(CallbackInfo ci) { if (!c.genericConfig().config().showOnScreenKeyboard) return; int keyboardHeight = (int) (this.height * 0.5f); - this.addRenderableWidget(this.keyboard = new KeyboardWidget(0, this.height - keyboardHeight, this.width, keyboardHeight, KeyboardLayouts.chat(), this, (AbstractSignEditScreen) (Object) this)); + this.addRenderableWidget(this.keyboard = new KeyboardWidget(0, this.height - keyboardHeight, this.width, keyboardHeight, KeyboardLayouts.simple(), this)); }); } - @WrapWithCondition(method = "init", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/AbstractSignEditScreen;addRenderableWidget(Lnet/minecraft/client/gui/components/events/GuiEventListener;)Lnet/minecraft/client/gui/components/events/GuiEventListener;")) + @WrapWithCondition( + method = "init", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/screens/inventory/AbstractSignEditScreen;addRenderableWidget(Lnet/minecraft/client/gui/components/events/GuiEventListener;)Lnet/minecraft/client/gui/components/events/GuiEventListener;" + ) + ) private boolean shouldAddDoneButton(AbstractSignEditScreen instance, GuiEventListener guiEventListener) { return this.keyboard == null; } @@ -99,7 +111,20 @@ private boolean shouldAddDoneButton(AbstractSignEditScreen instance, GuiEventLis } @Override - public boolean controlify$isKeyboardSupported() { - return this.keyboard != null; + public boolean controlify$supportsCursorMovement() { + return true; + } + + @Override + public boolean controlify$moveCursor(int amount) { + if (this.signField == null) return false; + + this.signField.moveByChars(amount); + return true; + } + + @Override + public ScreenProcessor screenProcessor() { + return screenProcessor; } } diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/sign/SignEditScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/sign/SignEditScreenMixin.java similarity index 90% rename from src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/sign/SignEditScreenMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/sign/SignEditScreenMixin.java index a948c27d..d498c6e2 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenkeyboard/sign/SignEditScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/impl/sign/SignEditScreenMixin.java @@ -1,10 +1,9 @@ -package dev.isxander.controlify.mixins.feature.screenkeyboard.sign; +package dev.isxander.controlify.mixins.feature.screenop.impl.sign; import com.llamalad7.mixinextras.expression.Definition; import com.llamalad7.mixinextras.expression.Expression; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.injector.ModifyReturnValue; -import dev.isxander.controlify.screenkeyboard.KeyboardSupportedMarker; import net.minecraft.client.gui.screens.inventory.AbstractSignEditScreen; import net.minecraft.client.gui.screens.inventory.SignEditScreen; import net.minecraft.world.level.block.entity.SignBlockEntity; @@ -13,7 +12,7 @@ import org.spongepowered.asm.mixin.injection.At; @Mixin(SignEditScreen.class) -public abstract class SignEditScreenMixin extends AbstractSignEditScreen implements KeyboardSupportedMarker { +public abstract class SignEditScreenMixin extends AbstractSignEditScreen { public SignEditScreenMixin(SignBlockEntity sign, boolean isFrontText, boolean isFiltered) { super(sign, isFrontText, isFiltered); diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractSignEditScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractSignEditScreenMixin.java deleted file mode 100644 index 6bd78966..00000000 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/AbstractSignEditScreenMixin.java +++ /dev/null @@ -1,19 +0,0 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; - -import dev.isxander.controlify.screenop.ScreenProcessor; -import dev.isxander.controlify.screenop.ScreenProcessorProvider; -import dev.isxander.controlify.screenop.compat.vanilla.AbstractSignEditScreenProcessor; -import net.minecraft.client.gui.screens.inventory.AbstractSignEditScreen; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; - -@Mixin(AbstractSignEditScreen.class) -public class AbstractSignEditScreenMixin implements ScreenProcessorProvider { - @Unique private final AbstractSignEditScreenProcessor screenProcessor = - new AbstractSignEditScreenProcessor((AbstractSignEditScreen) (Object) this); - - @Override - public ScreenProcessor screenProcessor() { - return screenProcessor; - } -} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/EditBoxMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/EditBoxMixin.java deleted file mode 100644 index ccd9b933..00000000 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/EditBoxMixin.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; - -import dev.isxander.controlify.screenop.ComponentProcessor; -import dev.isxander.controlify.screenop.ComponentProcessorProvider; -import dev.isxander.controlify.screenop.compat.vanilla.EditBoxComponentProcessor; -import net.minecraft.client.gui.components.EditBox; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; - -@Mixin(EditBox.class) -public class EditBoxMixin implements ComponentProcessorProvider { - @Unique private final ComponentProcessor processor = new EditBoxComponentProcessor(); - - @Override - public ComponentProcessor componentProcessor() { - return processor; - } -} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/JoinMultiplayerScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/JoinMultiplayerScreenMixin.java deleted file mode 100644 index e437f6de..00000000 --- a/src/main/java/dev/isxander/controlify/mixins/feature/screenop/vanilla/JoinMultiplayerScreenMixin.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.isxander.controlify.mixins.feature.screenop.vanilla; - -import dev.isxander.controlify.screenop.ScreenProcessor; -import dev.isxander.controlify.screenop.ScreenProcessorProvider; -import dev.isxander.controlify.screenop.compat.vanilla.JoinMultiplayerScreenProcessor; -import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen; -import net.minecraft.client.gui.screens.multiplayer.ServerSelectionList; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; - -@Mixin(JoinMultiplayerScreen.class) -public class JoinMultiplayerScreenMixin implements ScreenProcessorProvider { - @Shadow protected ServerSelectionList serverSelectionList; - - @Unique private final JoinMultiplayerScreenProcessor controlify$processor - = new JoinMultiplayerScreenProcessor((JoinMultiplayerScreen) (Object) this, () -> serverSelectionList); - - @Override - public ScreenProcessor screenProcessor() { - return controlify$processor; - } -} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutWithId.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutWithId.java deleted file mode 100644 index 233e6fe4..00000000 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayoutWithId.java +++ /dev/null @@ -1,6 +0,0 @@ -package dev.isxander.controlify.screenkeyboard; - -import net.minecraft.resources.ResourceLocation; - -public record KeyboardLayoutWithId(KeyboardLayout layout, ResourceLocation id) { -} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayouts.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayouts.java deleted file mode 100644 index 5cc60d6d..00000000 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardLayouts.java +++ /dev/null @@ -1,20 +0,0 @@ -package dev.isxander.controlify.screenkeyboard; - -import dev.isxander.controlify.Controlify; -import dev.isxander.controlify.utils.CUtil; -import net.minecraft.resources.ResourceLocation; - -public final class KeyboardLayouts { - - public static final ResourceLocation CHAT = CUtil.rl("chat"); - - public static KeyboardLayoutWithId chat() { - return Controlify.instance().keyboardLayoutManager().getLayout(CHAT); - } - - public static KeyboardLayoutWithId fallback() { - return new KeyboardLayoutWithId(FallbackKeyboardLayout.QWERTY, FallbackKeyboardLayout.ID); - } - - private KeyboardLayouts() {} -} diff --git a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardSupportedMarker.java b/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardSupportedMarker.java deleted file mode 100644 index 1148e090..00000000 --- a/src/main/java/dev/isxander/controlify/screenkeyboard/KeyboardSupportedMarker.java +++ /dev/null @@ -1,22 +0,0 @@ -package dev.isxander.controlify.screenkeyboard; - -public interface KeyboardSupportedMarker { - - boolean controlify$isKeyboardSupported(); - - static boolean isKeyboardSupported(Object obj) { - return obj instanceof KeyboardSupportedMarker marker && marker.controlify$isKeyboardSupported(); - } - - static boolean setKeyboardSupported(Object obj, boolean supported) { - if (obj instanceof Mutable mutable) { - mutable.controlify$setKeyboardSupported(supported); - return true; - } - return false; - } - - interface Mutable extends KeyboardSupportedMarker { - void controlify$setKeyboardSupported(boolean supported); - } -} diff --git a/src/main/java/dev/isxander/controlify/screenop/ComponentProcessor.java b/src/main/java/dev/isxander/controlify/screenop/ComponentProcessor.java index efa8db1d..707ee54f 100644 --- a/src/main/java/dev/isxander/controlify/screenop/ComponentProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/ComponentProcessor.java @@ -1,6 +1,7 @@ package dev.isxander.controlify.screenop; import dev.isxander.controlify.controller.ControllerEntity; +import dev.isxander.controlify.screenop.keyboard.ComponentKeyboardBehaviour; public interface ComponentProcessor extends ComponentProcessorProvider { ComponentProcessor EMPTY = new ComponentProcessor(){}; @@ -20,6 +21,10 @@ default boolean shouldKeepFocusOnKeyboardMode(ScreenProcessor screen) { return false; } + default ComponentKeyboardBehaviour getKeyboardBehaviour(ScreenProcessor screen, ControllerEntity controller) { + return ComponentKeyboardBehaviour.UNDEFINED; + } + @Override default ComponentProcessor componentProcessor() { return this; diff --git a/src/main/java/dev/isxander/controlify/screenop/ComponentProcessorProvider.java b/src/main/java/dev/isxander/controlify/screenop/ComponentProcessorProvider.java index 3035ef4b..a4d8ac4f 100644 --- a/src/main/java/dev/isxander/controlify/screenop/ComponentProcessorProvider.java +++ b/src/main/java/dev/isxander/controlify/screenop/ComponentProcessorProvider.java @@ -6,6 +6,10 @@ public interface ComponentProcessorProvider { ComponentProcessor componentProcessor(); static ComponentProcessor provide(GuiEventListener component) { + if (component == null) { + return ComponentProcessor.EMPTY; + } + if (component instanceof ComponentProcessorProvider provider) return provider.componentProcessor(); diff --git a/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java b/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java index 51425650..24d0788f 100644 --- a/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/ScreenProcessor.java @@ -10,7 +10,8 @@ import dev.isxander.controlify.controller.input.GamepadInputs; import dev.isxander.controlify.controller.input.InputComponent; import dev.isxander.controlify.mixins.feature.screenop.ScreenAccessor; -import dev.isxander.controlify.mixins.feature.screenop.vanilla.TabNavigationBarAccessor; +import dev.isxander.controlify.mixins.feature.screenop.impl.outofgame.TabNavigationBarAccessor; +import dev.isxander.controlify.screenop.keyboard.*; import dev.isxander.controlify.sound.ControlifyClientSounds; import dev.isxander.controlify.utils.HoldRepeatHelper; import dev.isxander.controlify.virtualmouse.VirtualMouseBehaviour; @@ -19,6 +20,7 @@ import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.components.tabs.Tab; import net.minecraft.client.gui.components.tabs.TabNavigationBar; @@ -28,6 +30,7 @@ import net.minecraft.client.resources.sounds.SimpleSoundInstance; import net.minecraft.network.chat.Component; import net.minecraft.sounds.SoundEvents; +import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; import java.util.*; @@ -179,7 +182,9 @@ protected void handleButtons(ControllerEntity controller) { boolean prevTouchpadPressed = input.stateThen().isButtonDown(GamepadInputs.TOUCHPAD_1_BUTTON); if (ControlifyBindings.GUI_PRESS.on(controller).guiPressed().get() || (vmouseEnabled && touchpadPressed && !prevTouchpadPressed)) { - screen.keyPressed(GLFW.GLFW_KEY_ENTER, 0, 0); + if (!this.tryOpenKeyboard(controller)) { + screen.keyPressed(GLFW.GLFW_KEY_ENTER, 0, 0); + } } if (screen.shouldCloseOnEsc() && ControlifyBindings.GUI_BACK.on(controller).guiPressed().get()) { playClackSound(); @@ -275,6 +280,30 @@ public void addEventListener(ScreenControllerEventListener listener) { eventListeners.add(listener); } + protected boolean tryOpenKeyboard(ControllerEntity controller) { + @Nullable GuiEventListener focused = screen.getFocused(); + var componentProcessor = ComponentProcessorProvider.provide(focused); + ComponentKeyboardBehaviour behaviour = componentProcessor.getKeyboardBehaviour(this, controller); + + switch (behaviour) { + case ComponentKeyboardBehaviour.DoNothing() -> { + // prevent pressing enter on select when handled + return true; + } + case ComponentKeyboardBehaviour.Undefined() -> { + return false; + } + case ComponentKeyboardBehaviour.Handled( + KeyboardLayoutWithId layout, + InputTarget inputTarget, + KeyboardOverlayScreen.KeyboardPositioner positioner + ) -> { + minecraft.setScreen(new KeyboardOverlayScreen(screen, layout, inputTarget, positioner)); + return true; + } + } + } + protected HoldRepeatHelper createHoldRepeatHelper() { return new HoldRepeatHelper(10, 3); } diff --git a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractSignEditScreenProcessor.java b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractSignEditScreenProcessor.java index 16d9fe5a..6823cd2d 100644 --- a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractSignEditScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AbstractSignEditScreenProcessor.java @@ -5,22 +5,60 @@ import dev.isxander.controlify.api.buttonguide.ButtonGuideApi; import dev.isxander.controlify.api.buttonguide.ButtonGuidePredicate; import dev.isxander.controlify.bindings.ControlifyBindings; +import dev.isxander.controlify.controller.ControllerEntity; import dev.isxander.controlify.screenop.ScreenProcessor; +import dev.isxander.controlify.virtualmouse.VirtualMouseHandler; +import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractButton; import net.minecraft.client.gui.screens.inventory.AbstractSignEditScreen; import net.minecraft.network.chat.CommonComponents; +import java.util.Optional; +import java.util.function.Consumer; + public class AbstractSignEditScreenProcessor extends ScreenProcessor { - public AbstractSignEditScreenProcessor(AbstractSignEditScreen screen) { + + private final Consumer moveCursorFunc; + + public AbstractSignEditScreenProcessor( + AbstractSignEditScreen screen, + Consumer moveCursorFunc + ) { super(screen); + this.moveCursorFunc = moveCursorFunc; + } + + @Override + protected void handleButtons(ControllerEntity controller) { + super.handleButtons(controller); + + // move cursor down a line + if (ControlifyBindings.GUI_SECONDARY_NAVI_DOWN.on(controller).justPressed()) { + this.moveCursorFunc.accept(1); + + playFocusChangeSound(); + } + + // move cursor up a line + if (ControlifyBindings.GUI_SECONDARY_NAVI_UP.on(controller).justPressed()) { + this.moveCursorFunc.accept(-1); + + playFocusChangeSound(); + } + } + + @Override + protected void render(ControllerEntity controller, GuiGraphics graphics, float tickDelta, Optional vmouse) { + } @Override protected void setInitialFocus() { - if (Controlify.instance().currentInputMode() == InputMode.MIXED) + if (Controlify.instance().currentInputMode() == InputMode.MIXED) { holdRepeatHelper.clearDelay(); - else + } else { super.setInitialFocus(); + } } @Override diff --git a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AddServerLikeScreenProcessor.java b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AddServerLikeScreenProcessor.java new file mode 100644 index 00000000..00a74e14 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/AddServerLikeScreenProcessor.java @@ -0,0 +1,78 @@ +package dev.isxander.controlify.screenop.compat.vanilla; + +import dev.isxander.controlify.api.buttonguide.ButtonGuideApi; +import dev.isxander.controlify.api.buttonguide.ButtonGuidePredicate; +import dev.isxander.controlify.bindings.ControlifyBindings; +import dev.isxander.controlify.controller.ControllerEntity; +import dev.isxander.controlify.screenop.ComponentProcessorProvider; +import dev.isxander.controlify.screenop.ScreenProcessor; +import dev.isxander.controlify.screenop.keyboard.KeyboardLayouts; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; + +import java.util.function.Supplier; + +public class AddServerLikeScreenProcessor extends ScreenProcessor { + + private final Supplier ipEditBoxSupplier; + private final Supplier