diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/MidnightControlsClient.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/MidnightControlsClient.java index 09c6718..847ac33 100644 --- a/common/src/main/java/eu/midnightdust/midnightcontrols/client/MidnightControlsClient.java +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/MidnightControlsClient.java @@ -26,6 +26,7 @@ import eu.midnightdust.midnightcontrols.client.mixin.KeyBindingIDAccessor; import eu.midnightdust.midnightcontrols.client.ring.ButtonBindingRingAction; import eu.midnightdust.midnightcontrols.client.ring.MidnightRing; import eu.midnightdust.midnightcontrols.client.util.platform.NetworkUtil; +import eu.midnightdust.midnightcontrols.client.virtualkeyboard.MouseClickInterceptor; import net.minecraft.client.gui.screen.Screen; import org.thinkingstudio.obsidianui.hud.HudManager; import eu.midnightdust.midnightcontrols.client.touch.TouchInput; @@ -75,6 +76,7 @@ public class MidnightControlsClient extends MidnightControls { public static final MidnightInput input = new MidnightInput(); public static final MidnightRing ring = new MidnightRing(); public static final MidnightReacharound reacharound = new MidnightReacharound(); + public static final MouseClickInterceptor clickInterceptor = new MouseClickInterceptor(); public static boolean isWayland; private static MidnightControlsHud hud; private static ControlsMode previousControlsMode; @@ -204,6 +206,7 @@ public class MidnightControlsClient extends MidnightControls { RainbowColor.tick(); TouchInput.tick(); } + /** * Called when opening a screen. */ diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/MidnightControlsConfig.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/MidnightControlsConfig.java index 8d210a9..85d4b9c 100644 --- a/common/src/main/java/eu/midnightdust/midnightcontrols/client/MidnightControlsConfig.java +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/MidnightControlsConfig.java @@ -112,6 +112,7 @@ public class MidnightControlsConfig extends MidnightConfig { @Condition(requiredOption = "virtualMouse", visibleButLocked = true) @Entry(category = SCREENS, name = "midnightcontrols.menu.virtual_mouse.skin") public static VirtualMouseSkin virtualMouseSkin = VirtualMouseSkin.DEFAULT_LIGHT; @Entry(category = SCREENS, name = "midnightcontrols.menu.hide_cursor") public static boolean hideNormalMouse = false; + @Entry(category = SCREENS, name = "midnightcontrols.menu.virtual_keyboard") public static boolean virtualKeyboard = false; @Entry(category = CONTROLLER, name = "Controller ID") @Hidden public static Object controllerID = 0; @Entry(category = CONTROLLER, name = "2nd Controller ID") @Hidden public static Object secondControllerID = -1; @Comment(category = TOUCH, centered = true, name="\uD83E\uDE84 Behaviour") public static Comment _touchBehaviour; @@ -402,6 +403,7 @@ public class MidnightControlsConfig extends MidnightConfig { unfocusedInput = false; virtualMouse = false; virtualMouseSkin = VirtualMouseSkin.DEFAULT_LIGHT; + virtualKeyboard = false; controllerID = 0; secondControllerID = -1; controllerType = ControllerType.DEFAULT; diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/MidnightControlsSettingsScreen.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/MidnightControlsSettingsScreen.java index a07a20e..9e06f94 100644 --- a/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/MidnightControlsSettingsScreen.java +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/MidnightControlsSettingsScreen.java @@ -65,6 +65,7 @@ public class MidnightControlsSettingsScreen extends SpruceScreen { private final SpruceOption eyeTrackingAsMouseOption; private final SpruceOption eyeTrackingDeadzone; private final SpruceOption virtualMouseOption; + private final SpruceOption virtualKeyboardOption; private final SpruceOption hideCursorOption; private final SpruceOption resetOption; private final SpruceOption advancedConfigOption; @@ -300,6 +301,8 @@ public class MidnightControlsSettingsScreen extends SpruceScreen { value -> MidnightControlsConfig.unfocusedInput = value, Text.translatable("midnightcontrols.menu.unfocused_input.tooltip")); this.virtualMouseOption = new SpruceToggleBooleanOption("midnightcontrols.menu.virtual_mouse", () -> MidnightControlsConfig.virtualMouse, value -> MidnightControlsConfig.virtualMouse = value, Text.translatable("midnightcontrols.menu.virtual_mouse.tooltip")); + this.virtualKeyboardOption = new SpruceToggleBooleanOption("midnightcontrols.menu.virtual_keyboard", () -> MidnightControlsConfig.virtualMouse, + value -> MidnightControlsConfig.virtualKeyboard = value, Text.translatable("midnightcontrols.menu.virtual_keyboard.tooltip")); this.hideCursorOption = new SpruceToggleBooleanOption("midnightcontrols.menu.hide_cursor", () -> MidnightControlsConfig.hideNormalMouse, value -> MidnightControlsConfig.hideNormalMouse = value, Text.translatable("midnightcontrols.menu.hide_cursor.tooltip")); // Touch options @@ -391,6 +394,7 @@ public class MidnightControlsSettingsScreen extends SpruceScreen { list.addSingleOptionEntry(this.yAxisRotationSpeedOption); list.addSingleOptionEntry(this.mouseSpeedOption); list.addSingleOptionEntry(this.virtualMouseOption); + list.addSingleOptionEntry(this.virtualKeyboardOption); list.addSingleOptionEntry(this.hideCursorOption); list.addSingleOptionEntry(this.joystickAsMouseOption); list.addSingleOptionEntry(this.eyeTrackingAsMouseOption); diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/mixin/AbstractSignEditScreenAccessor.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/mixin/AbstractSignEditScreenAccessor.java new file mode 100644 index 0000000..fa5036c --- /dev/null +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/mixin/AbstractSignEditScreenAccessor.java @@ -0,0 +1,15 @@ +package eu.midnightdust.midnightcontrols.client.mixin; + +import net.minecraft.block.entity.SignText; +import net.minecraft.client.gui.screen.ingame.AbstractSignEditScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(AbstractSignEditScreen.class) +public interface AbstractSignEditScreenAccessor { + @Accessor("text") + SignText midnightcontrols$getText(); + + @Accessor("text") + void midnightcontrols$setText(SignText text); +} diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/mixin/BookEditScreenAccessor.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/mixin/BookEditScreenAccessor.java new file mode 100644 index 0000000..e44a57c --- /dev/null +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/mixin/BookEditScreenAccessor.java @@ -0,0 +1,28 @@ +package eu.midnightdust.midnightcontrols.client.mixin; + +import net.minecraft.client.gui.screen.ingame.BookEditScreen; +import net.minecraft.client.util.SelectionManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(BookEditScreen.class) +public interface BookEditScreenAccessor { + @Accessor("signing") + boolean midnightcontrols$isSigning(); + + @Accessor("title") + String midnightcontrols$getTitle(); + + @Accessor("title") + void midnightcontrols$setTitle(String title); + + @Accessor("currentPageSelectionManager") + SelectionManager midnightcontrols$getCurrentPageSelectionManager(); + + @Invoker("getCurrentPageContent") + String midnightcontrols$getCurrentPageContent(); + + @Invoker("setPageContent") + void midnightcontrols$setPageContent(String newContent); +} diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/mixin/CreativeInventoryScreenAccessor.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/mixin/CreativeInventoryScreenAccessor.java index 94961cf..2ab6c1d 100644 --- a/common/src/main/java/eu/midnightdust/midnightcontrols/client/mixin/CreativeInventoryScreenAccessor.java +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/mixin/CreativeInventoryScreenAccessor.java @@ -58,4 +58,10 @@ public interface CreativeInventoryScreenAccessor { */ @Invoker("hasScrollbar") boolean midnightcontrols$hasScrollbar(); + + /** + * Triggers searching the creative inventory from the current value of the internal {@link net.minecraft.client.gui.widget.TextFieldWidget} + */ + @Invoker("search") + void midnightcontrols$search(); } diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/KeyboardLayout.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/KeyboardLayout.java new file mode 100644 index 0000000..82d9b0b --- /dev/null +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/KeyboardLayout.java @@ -0,0 +1,60 @@ +package eu.midnightdust.midnightcontrols.client.virtualkeyboard; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class KeyboardLayout { + + public static KeyboardLayout QWERTY = new KeyboardLayout(createQwertyLetterLayout(), createSymbolLayout()); + + private final List> letters; + private final List> symbols; + + private KeyboardLayout(List> letters, List> symbols) { + this.letters = letters; + this.symbols = symbols; + } + + public List> getLetters() { + return letters; + } + + public List> getSymbols() { + return symbols; + } + + private static List> createQwertyLetterLayout() { + List> letters = new ArrayList<>(); + letters.add(Arrays.asList( + "q", "w", "e", "r", "t", + "y", "u", "i", "o", "p" + )); + letters.add(Arrays.asList( + "a", "s", "d", "f", "g", + "h", "j", "k", "l" + )); + letters.add(Arrays.asList( + "z", "x", "c", "v", + "b", "n", "m" + )); + return letters; + } + + private static List> createSymbolLayout() { + List> symbols = new ArrayList<>(); + symbols.add(Arrays.asList( + "1", "2", "3", "4", "5", + "6", "7", "8", "9", "0" + )); + symbols.add(Arrays.asList( + "@", "#", "$", "%", "&", + "*", "-", "+", "(", ")" + )); + symbols.add(Arrays.asList( + "!", "\"", "'", ":", ";", + ",", ".", "?", "/" + )); + return symbols; + } +} \ No newline at end of file diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/MouseClickInterceptor.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/MouseClickInterceptor.java new file mode 100644 index 0000000..85441c2 --- /dev/null +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/MouseClickInterceptor.java @@ -0,0 +1,36 @@ +package eu.midnightdust.midnightcontrols.client.virtualkeyboard; + +import eu.midnightdust.midnightcontrols.client.virtualkeyboard.clickhandler.AbstractScreenClickHandler; +import eu.midnightdust.midnightcontrols.client.virtualkeyboard.clickhandler.BookEditScreenClickHandler; +import eu.midnightdust.midnightcontrols.client.virtualkeyboard.clickhandler.DefaultScreenClickHandler; +import eu.midnightdust.midnightcontrols.client.virtualkeyboard.clickhandler.SignEditScreenClickHandler; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.BookEditScreen; +import net.minecraft.client.gui.screen.ingame.SignEditScreen; + +import java.util.HashMap; +import java.util.Map; + + +public class MouseClickInterceptor { + + private final Map, AbstractScreenClickHandler> clickHandlers; + + public MouseClickInterceptor() { + this.clickHandlers = new HashMap<>(); + this.clickHandlers.put(BookEditScreen.class, new BookEditScreenClickHandler()); + this.clickHandlers.put(SignEditScreen.class, new SignEditScreenClickHandler()); + this.clickHandlers.put(Screen.class, new DefaultScreenClickHandler()); + } + + @SuppressWarnings("unchecked") + public void intercept(T screen, double mouseX, double mouseY) { + AbstractScreenClickHandler handler = (AbstractScreenClickHandler) clickHandlers.get(screen.getClass()); + + if (handler == null) { + handler = (AbstractScreenClickHandler) clickHandlers.get(Screen.class); + } + + handler.handle(screen, mouseX, mouseY); + } +} diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/AbstractScreenClickHandler.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/AbstractScreenClickHandler.java new file mode 100644 index 0000000..f347f05 --- /dev/null +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/AbstractScreenClickHandler.java @@ -0,0 +1,7 @@ +package eu.midnightdust.midnightcontrols.client.virtualkeyboard.clickhandler; + +import net.minecraft.client.gui.screen.Screen; + +public abstract class AbstractScreenClickHandler { + public abstract void handle(T screen, double mouseX, double mouseY); +} diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/BookEditScreenClickHandler.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/BookEditScreenClickHandler.java new file mode 100644 index 0000000..dbb1ebf --- /dev/null +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/BookEditScreenClickHandler.java @@ -0,0 +1,36 @@ +package eu.midnightdust.midnightcontrols.client.virtualkeyboard.clickhandler; + +import eu.midnightdust.midnightcontrols.client.mixin.BookEditScreenAccessor; +import eu.midnightdust.midnightcontrols.client.virtualkeyboard.gui.VirtualKeyboardScreen; +import net.minecraft.client.gui.screen.ingame.BookEditScreen; + +import static eu.midnightdust.midnightcontrols.client.MidnightControlsClient.client; + +public class BookEditScreenClickHandler extends AbstractScreenClickHandler { + @Override + public void handle(BookEditScreen screen, double mouseX, double mouseY) { + // don't open the keyboard if a UI element was clicked + if(screen.hoveredElement(mouseX, mouseY).isPresent()) { + return; + } + + var accessor = (BookEditScreenAccessor) screen; + + VirtualKeyboardScreen virtualKeyboardScreen; + if(accessor.midnightcontrols$isSigning()) { + virtualKeyboardScreen = new VirtualKeyboardScreen(accessor.midnightcontrols$getTitle(), (text) -> { + client.setScreen(screen); + accessor.midnightcontrols$setTitle(text); + }, true); + } + else { + virtualKeyboardScreen = new VirtualKeyboardScreen(accessor.midnightcontrols$getCurrentPageContent(), (text) -> { + client.setScreen(screen); + accessor.midnightcontrols$setPageContent(text); + accessor.midnightcontrols$getCurrentPageSelectionManager().putCursorAtEnd(); + }, true); + } + + client.setScreen(virtualKeyboardScreen); + } +} diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/DefaultScreenClickHandler.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/DefaultScreenClickHandler.java new file mode 100644 index 0000000..78842e2 --- /dev/null +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/DefaultScreenClickHandler.java @@ -0,0 +1,144 @@ +package eu.midnightdust.midnightcontrols.client.virtualkeyboard.clickhandler; + +import eu.midnightdust.midnightcontrols.client.mixin.CreativeInventoryScreenAccessor; +import eu.midnightdust.midnightcontrols.client.virtualkeyboard.gui.VirtualKeyboardScreen; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.ParentElement; +import net.minecraft.client.gui.screen.ChatScreen; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen; +import org.lwjgl.glfw.GLFW; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static eu.midnightdust.midnightcontrols.client.MidnightControlsClient.client; + +public class DefaultScreenClickHandler extends AbstractScreenClickHandler { + + private Screen parentScreen; + private List textFieldElementPath; + + @Override + public void handle(Screen screen, double mouseX, double mouseY) { + var textField = findClickedTextField(screen.children(), mouseX, mouseY); + if (textField == null) { + return; + } + + this.parentScreen = screen; + this.textFieldElementPath = calculatePathToElement(screen, textField.asElement()); + + var virtualKeyboardScreen = new VirtualKeyboardScreen(textField.getText(), this::handleKeyboardClose, false); + client.setScreen(virtualKeyboardScreen); + } + + private void handleKeyboardClose(String newText) { + if (this.parentScreen == null || this.textFieldElementPath == null) { + return; + } + + client.setScreen(this.parentScreen); + TextFieldWrapper textField = findTextFieldByPath(this.parentScreen, this.textFieldElementPath); + if (textField == null) { + return; + } + + textField.setText(newText); + + switch (this.parentScreen) { + case CreativeInventoryScreen creativeInventoryScreen -> { + var accessor = (CreativeInventoryScreenAccessor) creativeInventoryScreen; + accessor.midnightcontrols$search(); + } + case ChatScreen chatScreen -> { + // send the chat message + chatScreen.keyPressed(GLFW.GLFW_KEY_ENTER, 0, 0); + } + default -> { + } + } + } + + + private TextFieldWrapper findClickedTextField(List elements, double mouseX, double mouseY) { + for (Element element : elements) { + if (TextFieldWrapper.isValidTextField(element)) { + TextFieldWrapper textField = new TextFieldWrapper(element); + if (textField.isMouseOver(mouseX, mouseY) && textField.isFocused()) { + return textField; + } + } + + if (element instanceof ParentElement parentElement) { + TextFieldWrapper found = findClickedTextField(parentElement.children(), mouseX, mouseY); + if (found != null) { + return found; + } + } + } + + return null; + } + + /** + * Calculates the path between a parent and a target in the UI hierarchy + */ + protected List calculatePathToElement(Element parent, Element target) { + if (!(parent instanceof ParentElement parentElement)) { + return null; + } + + List children = parentElement.children(); + + for (int i = 0; i < children.size(); i++) { + Element child = children.get(i); + + if (child == target) { + return Collections.singletonList(i); + } + + if (child instanceof ParentElement) { + List subPath = calculatePathToElement(child, target); + if (subPath != null) { + List fullPath = new ArrayList<>(subPath.size() + 1); + fullPath.add(i); + fullPath.addAll(subPath); + return fullPath; + } + } + } + + return null; + } + + protected TextFieldWrapper findTextFieldByPath(Element parent, List path) { + if (path == null || path.isEmpty()) { + return null; + } + + if (!(parent instanceof ParentElement parentElement)) { + return null; + } + + List children = parentElement.children(); + int index = path.get(0); + + if (index < 0 || index >= children.size()) { + return null; + } + + Element child = children.get(index); + + if (path.size() == 1) { + return TextFieldWrapper.isValidTextField(child) ? new TextFieldWrapper(child) : null; + } + + if (child instanceof ParentElement) { + return findTextFieldByPath(child, path.subList(1, path.size())); + } + + return null; + } +} diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/SignEditScreenClickHandler.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/SignEditScreenClickHandler.java new file mode 100644 index 0000000..ced8546 --- /dev/null +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/SignEditScreenClickHandler.java @@ -0,0 +1,16 @@ +package eu.midnightdust.midnightcontrols.client.virtualkeyboard.clickhandler; + +import eu.midnightdust.midnightcontrols.client.mixin.AbstractSignEditScreenAccessor; +import net.minecraft.client.gui.screen.ingame.SignEditScreen; + +public class SignEditScreenClickHandler extends AbstractScreenClickHandler { + @Override + public void handle(SignEditScreen screen, double mouseX, double mouseY) { + // don't open the keyboard if a UI element was clicked + if(screen.hoveredElement(mouseX, mouseY).isPresent()) { + return; + } + + var accessor = (AbstractSignEditScreenAccessor) screen; + } +} diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/TextFieldWrapper.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/TextFieldWrapper.java new file mode 100644 index 0000000..f1190cb --- /dev/null +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/clickhandler/TextFieldWrapper.java @@ -0,0 +1,77 @@ +package eu.midnightdust.midnightcontrols.client.virtualkeyboard.clickhandler; + +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.widget.TextFieldWidget; +import org.thinkingstudio.obsidianui.widget.text.SpruceTextFieldWidget; + +public record TextFieldWrapper(Object textField) { + + public TextFieldWrapper { + if (!isValidTextField(textField)) { + throw new IllegalArgumentException("Type " + textField.getClass() + " is not marked as a valid text field"); + } + } + + Element asElement() { + return (Element) textField; + } + + String getText() { + switch (textField) { + case SpruceTextFieldWidget spruceTextField -> { + return spruceTextField.getText(); + } + case TextFieldWidget vanillaTextField -> { + return vanillaTextField.getText(); + } + default -> { + return null; + } + } + } + + void setText(String text) { + switch (textField) { + case SpruceTextFieldWidget spruceTextField -> { + spruceTextField.setText(text); + } + case TextFieldWidget vanillaTextField -> { + vanillaTextField.setText(text); + } + default -> { + } + } + } + + boolean isMouseOver(double mouseX, double mouseY) { + switch (textField) { + case SpruceTextFieldWidget spruceTextField -> { + return spruceTextField.isMouseOver(mouseX, mouseY); + } + case TextFieldWidget vanillaTextField -> { + return vanillaTextField.isMouseOver(mouseX, mouseY); + } + default -> { + return false; + } + } + } + + boolean isFocused() { + switch (textField) { + case SpruceTextFieldWidget spruceTextField -> { + return spruceTextField.isFocused(); + } + case TextFieldWidget vanillaTextField -> { + return vanillaTextField.isFocused(); + } + default -> { + return false; + } + } + } + + static boolean isValidTextField(Object textField) { + return textField instanceof TextFieldWidget || textField instanceof SpruceTextFieldWidget; + } +} diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/gui/VirtualKeyboardScreen.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/gui/VirtualKeyboardScreen.java new file mode 100644 index 0000000..bb26353 --- /dev/null +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/virtualkeyboard/gui/VirtualKeyboardScreen.java @@ -0,0 +1,300 @@ +package eu.midnightdust.midnightcontrols.client.virtualkeyboard.gui; + +import eu.midnightdust.midnightcontrols.client.virtualkeyboard.KeyboardLayout; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import org.thinkingstudio.obsidianui.Position; +import org.thinkingstudio.obsidianui.SpruceTexts; +import org.thinkingstudio.obsidianui.screen.SpruceScreen; +import org.thinkingstudio.obsidianui.widget.SpruceButtonWidget; +import org.thinkingstudio.obsidianui.widget.container.SpruceContainerWidget; +import org.thinkingstudio.obsidianui.widget.text.SpruceTextAreaWidget; + +import java.util.List; + +public class VirtualKeyboardScreen extends SpruceScreen { + + @FunctionalInterface + public interface CloseCallback { + void onClose(String text); + } + + private static final int STANDARD_KEY_WIDTH = 20; + private static final int SPECIAL_KEY_WIDTH = (int) (STANDARD_KEY_WIDTH * 1.5); + private static final int KEY_HEIGHT = 20; + private static final int HORIZONTAL_SPACING = 2; + private static final int VERTICAL_SPACING = 4; + private static final int CONTAINER_PADDING = 10; + + // Key symbols + private static final String BACKSPACE_SYMBOL = "\b"; + private static final String NEWLINE_SYMBOL = "\n"; + private static final String SPACE_SYMBOL = " "; + + private final StringBuilder buffer; + private final CloseCallback closeCallback; + private final KeyboardLayout layout; + private final boolean newLineSupport; + + private boolean capsMode; + private boolean symbolMode; + private SpruceTextAreaWidget bufferDisplayArea; + private SpruceContainerWidget keyboardContainer; + + public VirtualKeyboardScreen(String initialText, CloseCallback closeCallback, boolean newLineSupport) { + super(Text.literal("Virtual Keyboard")); + this.buffer = new StringBuilder(initialText); + this.closeCallback = closeCallback; + this.layout = KeyboardLayout.QWERTY; + this.capsMode = false; + this.symbolMode = false; + this.newLineSupport = newLineSupport; + } + + @Override + protected void init() { + super.init(); + + this.bufferDisplayArea = createBufferDisplayArea(); + this.addDrawableChild(this.bufferDisplayArea); + + rebuildKeyboard(); + + int doneButtonY = this.keyboardContainer.getY() + this.keyboardContainer.getHeight() + VERTICAL_SPACING * 2; + this.addDrawableChild( + new SpruceButtonWidget( + Position.of(this, this.width / 2 - 50, doneButtonY), + 100, + 20, + SpruceTexts.GUI_DONE, + btn -> this.close() + ) + ); + } + + @Override + public void render(DrawContext drawContext, int mouseX, int mouseY, float delta) { + this.renderBackground(drawContext, mouseX, mouseY, delta); + super.render(drawContext, mouseX, mouseY, delta); + } + + @Override + public boolean shouldPause() { + return false; + } + + @Override + public void close() { + super.close(); + if (this.closeCallback != null) { + this.closeCallback.onClose(this.buffer.toString()); + } + } + + private void rebuildKeyboard() { + if (this.keyboardContainer != null) { + this.remove(this.keyboardContainer); + } + + var layoutKeys = getActiveKeyLayout(); + var keyboardContainer = createKeyboardContainer(layoutKeys); + + addLayoutRows(keyboardContainer, layoutKeys); + addFunctionKeys(keyboardContainer); + addBottomRow(keyboardContainer); + + this.keyboardContainer = keyboardContainer; + this.addDrawableChild(this.keyboardContainer); + } + + private SpruceContainerWidget createKeyboardContainer(List> layoutKeys) { + int containerWidth = this.width; + int totalKeyboardHeight = calculateKeyboardHeight(layoutKeys); + int keyboardY = this.bufferDisplayArea.getY() + this.bufferDisplayArea.getHeight() + VERTICAL_SPACING * 2; + + return new SpruceContainerWidget( + Position.of(0, keyboardY), + containerWidth, + totalKeyboardHeight + ); + } + + private SpruceTextAreaWidget createBufferDisplayArea() { + int lineCount = this.newLineSupport ? 4 : 1; + int bufferX = this.width / 2 - 100; + int bufferY = this.height / 4 - VERTICAL_SPACING * 5 - 5; + int bufferWidth = 200; + int desiredHeight = (this.textRenderer.fontHeight + 2) * lineCount + 6; + + var bufferDisplay = new SpruceTextAreaWidget( + Position.of(bufferX, bufferY), + bufferWidth, + desiredHeight, + Text.literal("Buffer Display") + ); + bufferDisplay.setText(this.buffer.toString()); + bufferDisplay.setEditable(false); + bufferDisplay.setUneditableColor(0xFFFFFFFF); + bufferDisplay.setDisplayedLines(lineCount); + bufferDisplay.setCursorToEnd(); + + return bufferDisplay; + } + + private int calculateKeyboardHeight(List> keyRows) { + return keyRows.size() * (KEY_HEIGHT + VERTICAL_SPACING) + + (KEY_HEIGHT + VERTICAL_SPACING) + // space for bottom row + CONTAINER_PADDING * 2; // top and bottom padding + } + + private void addLayoutRows(SpruceContainerWidget container, List> keyLayoutRows) { + int currentY = CONTAINER_PADDING; + + for (List row : keyLayoutRows) { + int rowWidth = calculateRowWidth(row); + // center row + int currentX = (container.getWidth() - rowWidth) / 2; + + for (String key : row) { + String displayText = (this.capsMode && !this.symbolMode) ? key.toUpperCase() : key; + container.addChild( + new SpruceButtonWidget( + Position.of(currentX, currentY), + STANDARD_KEY_WIDTH, + KEY_HEIGHT, + Text.literal(displayText), + btn -> handleKeyPress(displayText) + ) + ); + + currentX += STANDARD_KEY_WIDTH + HORIZONTAL_SPACING; + } + + currentY += KEY_HEIGHT + VERTICAL_SPACING; + } + } + + private int calculateRowWidth(List row) { + int rowWidth = 0; + for (int i = 0; i < row.size(); i++) { + rowWidth += STANDARD_KEY_WIDTH; + // padding + if (i < row.size() - 1) { + rowWidth += HORIZONTAL_SPACING; + } + } + return rowWidth; + } + + private void addFunctionKeys(SpruceContainerWidget container) { + List firstRow = getActiveKeyLayout().get(0); + int firstRowWidth = calculateRowWidth(firstRow); + + // position backspace at the right of the first row + int backspaceWidth = (int) (STANDARD_KEY_WIDTH * 1.5); + int backspaceX = (container.getWidth() + firstRowWidth) / 2 + HORIZONTAL_SPACING; + + container.addChild( + new SpruceButtonWidget( + Position.of(backspaceX, CONTAINER_PADDING), + backspaceWidth, + KEY_HEIGHT, + Text.literal("←"), + btn -> handleKeyPress(BACKSPACE_SYMBOL) + ) + ); + + + if (this.newLineSupport) { + // position newline at the right of the second row + List secondRow = getActiveKeyLayout().get(1); + int newlineWidth = (int) (STANDARD_KEY_WIDTH * 1.5); + int secondRowWidth = calculateRowWidth(secondRow); + int newlineX = (container.getWidth() + secondRowWidth) / 2 + HORIZONTAL_SPACING; + int newlineY = CONTAINER_PADDING + (KEY_HEIGHT + VERTICAL_SPACING); + + container.addChild( + new SpruceButtonWidget( + Position.of(newlineX, newlineY), + newlineWidth, + KEY_HEIGHT, + Text.literal("⏎"), + btn -> handleKeyPress(NEWLINE_SYMBOL) + ) + ); + } + } + + private void addBottomRow(SpruceContainerWidget container) { + // calculate positions for bottom row + int rowY = CONTAINER_PADDING + getActiveKeyLayout().size() * (KEY_HEIGHT + VERTICAL_SPACING); + + // space bar - wide key in the middle + double spaceWidthFactor = 5.0; + int spaceKeyWidth = (int) (STANDARD_KEY_WIDTH * spaceWidthFactor); + int spaceX = (container.getWidth() - spaceKeyWidth) / 2; + + container.addChild( + new SpruceButtonWidget( + Position.of(spaceX, rowY), + spaceKeyWidth, + KEY_HEIGHT, + Text.literal("Space"), + btn -> handleKeyPress(SPACE_SYMBOL) + ) + ); + + // caps key - left of space + if (!this.symbolMode) { + int capsX = spaceX - SPECIAL_KEY_WIDTH - HORIZONTAL_SPACING * 2; + var capsModeButton = new SpruceButtonWidget( + Position.of(capsX, rowY), + SPECIAL_KEY_WIDTH, + KEY_HEIGHT, + Text.literal(this.capsMode ? "caps" : "CAPS"), + btn -> toggleCapsMode()); + + container.addChild(capsModeButton); + } + + // symbols key - right of space + int symbolsX = spaceX + spaceKeyWidth + HORIZONTAL_SPACING * 2; + var symbolModeButton = new SpruceButtonWidget( + Position.of(symbolsX, rowY), + SPECIAL_KEY_WIDTH, + KEY_HEIGHT, + Text.literal(this.symbolMode ? "ABC" : "123?!"), + btn -> toggleSymbolMode() + ); + container.addChild(symbolModeButton); + } + + private void handleKeyPress(String key) { + if (key.equals(BACKSPACE_SYMBOL)) { + if (!this.buffer.isEmpty()) { + this.buffer.deleteCharAt(buffer.length() - 1); + } + } else { + this.buffer.append(key); + } + + if (this.bufferDisplayArea != null) { + this.bufferDisplayArea.setText(this.buffer.toString()); + this.bufferDisplayArea.setCursorToEnd(); + } + } + + private List> getActiveKeyLayout() { + return this.symbolMode ? this.layout.getSymbols() : this.layout.getLetters(); + } + + private void toggleCapsMode() { + this.capsMode = !this.capsMode; + rebuildKeyboard(); + } + + private void toggleSymbolMode() { + this.symbolMode = !this.symbolMode; + rebuildKeyboard(); + } +} \ No newline at end of file diff --git a/common/src/main/resources/assets/midnightcontrols/lang/en_us.json b/common/src/main/resources/assets/midnightcontrols/lang/en_us.json index 99ff087..dfe2b56 100644 --- a/common/src/main/resources/assets/midnightcontrols/lang/en_us.json +++ b/common/src/main/resources/assets/midnightcontrols/lang/en_us.json @@ -220,6 +220,8 @@ "midnightcontrols.menu.virtual_mouse": "Virtual Mouse", "midnightcontrols.menu.virtual_mouse.tooltip": "Enables the virtual mouse, which is useful during splitscreen.", "midnightcontrols.menu.virtual_mouse.skin": "Virtual Mouse Skin", + "midnightcontrols.menu.virtual_keyboard": "Virtual Keyboard", + "midnightcontrols.menu.virtual_keyboard.tooltip": "Enables a virtual on-screen keyboard", "midnightcontrols.menu.hide_cursor": "Hide Normal Mouse Cursor", "midnightcontrols.menu.hide_cursor.tooltip": "Hides the normal mouse cursor, leaving only the virtual mouse visible.", "midnightcontrols.narrator.unbound": "Unbound %s", diff --git a/common/src/main/resources/midnightcontrols.mixins.json b/common/src/main/resources/midnightcontrols.mixins.json index 6b902dd..c745ebd 100644 --- a/common/src/main/resources/midnightcontrols.mixins.json +++ b/common/src/main/resources/midnightcontrols.mixins.json @@ -4,6 +4,7 @@ "compatibilityLevel": "JAVA_21", "client": [ "AdvancementsScreenAccessor", + "BookEditScreenAccessor", "ChatScreenMixin", "ClickableWidgetAccessor", "ClientPlayerEntityMixin", @@ -23,6 +24,7 @@ "RecipeBookScreenAccessor", "RecipeBookWidgetAccessor", "ScreenMixin", + "AbstractSignEditScreenAccessor", "TabNavigationWidgetAccessor", "WorldRendererMixin", "AbstractBlockAccessor" diff --git a/fabric/src/main/java/eu/midnightdust/midnightcontrols/fabric/MidnightControlsClientFabric.java b/fabric/src/main/java/eu/midnightdust/midnightcontrols/fabric/MidnightControlsClientFabric.java index 60d43ac..7c9f04c 100644 --- a/fabric/src/main/java/eu/midnightdust/midnightcontrols/fabric/MidnightControlsClientFabric.java +++ b/fabric/src/main/java/eu/midnightdust/midnightcontrols/fabric/MidnightControlsClientFabric.java @@ -3,6 +3,7 @@ package eu.midnightdust.midnightcontrols.fabric; import eu.midnightdust.midnightcontrols.MidnightControlsConstants; import eu.midnightdust.midnightcontrols.client.MidnightControlsClient; import eu.midnightdust.midnightcontrols.client.MidnightControlsConfig; +import eu.midnightdust.midnightcontrols.fabric.event.MouseClickListener; import eu.midnightdust.midnightcontrols.packet.ControlsModePayload; import eu.midnightdust.midnightcontrols.packet.FeaturePayload; import eu.midnightdust.midnightcontrols.packet.HelloPayload; @@ -11,11 +12,12 @@ import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.ScreenMouseEvents; import net.fabricmc.fabric.api.resource.ResourceManagerHelper; import net.fabricmc.fabric.api.resource.ResourcePackActivationType; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; -import org.thinkingstudio.obsidianui.fabric.event.OpenScreenCallback; import java.util.Optional; @@ -51,6 +53,9 @@ public class MidnightControlsClientFabric implements ClientModInitializer { ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> MidnightControlsClient.onLeave()); ClientTickEvents.START_CLIENT_TICK.register(MidnightControlsClient::onTick); + ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> { + ScreenMouseEvents.allowMouseClick(screen).register(new MouseClickListener(screen)); + }); FabricLoader.getInstance().getModContainer(MidnightControlsConstants.NAMESPACE).ifPresent(modContainer -> { ResourceManagerHelper.registerBuiltinResourcePack(id("bedrock"), modContainer, ResourcePackActivationType.NORMAL); diff --git a/fabric/src/main/java/eu/midnightdust/midnightcontrols/fabric/event/MouseClickListener.java b/fabric/src/main/java/eu/midnightdust/midnightcontrols/fabric/event/MouseClickListener.java new file mode 100644 index 0000000..d1e22e8 --- /dev/null +++ b/fabric/src/main/java/eu/midnightdust/midnightcontrols/fabric/event/MouseClickListener.java @@ -0,0 +1,40 @@ +package eu.midnightdust.midnightcontrols.fabric.event; +import eu.midnightdust.midnightcontrols.client.MidnightControlsConfig; +import net.fabricmc.fabric.api.client.screen.v1.ScreenMouseEvents; +import net.minecraft.client.gui.screen.Screen; + +import static eu.midnightdust.midnightcontrols.client.MidnightControlsClient.clickInterceptor; + +public class MouseClickListener implements ScreenMouseEvents.AllowMouseClick { + private final Screen screen; + + public MouseClickListener(Screen screen) { + this.screen = screen; + } + + @Override + public boolean allowMouseClick(Screen screen, double mouseX, double mouseY, int button) { + if(MidnightControlsConfig.virtualKeyboard) { + clickInterceptor.intercept(screen, mouseX, mouseY); + } + return true; + } + + // Add equals and hashCode to prevent duplicate registrations + @Override + public boolean equals(Object obj) { + if (obj instanceof MouseClickListener) { + return ((MouseClickListener) obj).screen == this.screen; + } + return false; + } + + @Override + public int hashCode() { + return screen.hashCode(); + } + + + + +} diff --git a/neoforge/src/main/java/eu/midnightdust/midnightcontrols/neoforge/MidnightControlsClientNeoforge.java b/neoforge/src/main/java/eu/midnightdust/midnightcontrols/neoforge/MidnightControlsClientNeoforge.java index 9bd1e20..b98e286 100644 --- a/neoforge/src/main/java/eu/midnightdust/midnightcontrols/neoforge/MidnightControlsClientNeoforge.java +++ b/neoforge/src/main/java/eu/midnightdust/midnightcontrols/neoforge/MidnightControlsClientNeoforge.java @@ -5,6 +5,7 @@ import eu.midnightdust.midnightcontrols.client.MidnightControlsConfig; import eu.midnightdust.midnightcontrols.client.util.platform.NetworkUtil; import eu.midnightdust.midnightcontrols.packet.ControlsModePayload; import eu.midnightdust.midnightcontrols.packet.HelloPayload; +import net.minecraft.client.gui.screen.Screen; import net.minecraft.resource.DirectoryResourcePack; import net.minecraft.resource.ResourcePackInfo; import net.minecraft.resource.ResourcePackPosition; @@ -21,6 +22,7 @@ import net.neoforged.fml.common.Mod; import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; import net.neoforged.neoforge.client.event.ClientTickEvent; import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import net.neoforged.neoforge.client.event.ScreenEvent; import net.neoforged.neoforge.event.AddPackFindersEvent; import net.neoforged.neoforgespi.locating.IModFile; @@ -34,6 +36,8 @@ import static eu.midnightdust.midnightcontrols.client.MidnightControlsClient.BIN import static eu.midnightdust.midnightcontrols.client.MidnightControlsClient.BINDING_LOOK_UP; import static eu.midnightdust.midnightcontrols.client.MidnightControlsClient.BINDING_RING; import static eu.midnightdust.midnightcontrols.client.MidnightControlsClient.client; +import static eu.midnightdust.midnightcontrols.client.MidnightControlsClient.clickInterceptor; + @Mod(value = NAMESPACE, dist = Dist.CLIENT) public class MidnightControlsClientNeoforge { @@ -90,5 +94,15 @@ public class MidnightControlsClientNeoforge { public static void startClientTick(ClientTickEvent.Pre event) { MidnightControlsClient.onTick(client); } + @SubscribeEvent + public static void onMouseButtonPressed(ScreenEvent.MouseButtonPressed.Pre event) { + if (MidnightControlsConfig.virtualKeyboard && !event.isCanceled()) { + Screen screen = event.getScreen(); + double mouseX = event.getMouseX(); + double mouseY = event.getMouseY(); + + clickInterceptor.intercept(screen, mouseX, mouseY); + } + } } }