diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/VirtualKeyboardScreen.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/VirtualKeyboardScreen.java deleted file mode 100644 index 55ca8c4..0000000 --- a/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/VirtualKeyboardScreen.java +++ /dev/null @@ -1,108 +0,0 @@ -package eu.midnightdust.midnightcontrols.client.gui; - -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.widget.TextFieldWidget; -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; - -public class VirtualKeyboardScreen extends SpruceScreen { - private SpruceContainerWidget container; - private TextFieldWidget bufferDisplay; - private final StringBuilder buffer; - private final CloseCallback closeCallback; - - @FunctionalInterface - public interface CloseCallback { - void onClose(String text); - } - - public VirtualKeyboardScreen(String initialText, CloseCallback closeCallback) { - super(Text.literal("Virtual Keyboard")); - - this.buffer = new StringBuilder(initialText); - this.closeCallback = closeCallback; - } - - @Override - protected void init() { - super.init(); - - this.bufferDisplay = new TextFieldWidget(this.textRenderer, this.width / 2 - 100, this.height / 4 - 40, 200, 20, Text.literal("")); - this.bufferDisplay.setEditable(false); - this.bufferDisplay.setMaxLength(1024); - this.bufferDisplay.setText(buffer.toString()); - this.addDrawableChild(this.bufferDisplay); - - rebuildKeyboard(); - - this.addDrawableChild(container); - this.addDrawableChild(new SpruceButtonWidget(Position.of(this, this.width / 2 - 50, this.height - 30), 100, 20, SpruceTexts.GUI_DONE, btn -> this.close())); - } - - @Override - public void render(DrawContext drawContext, int mouseX, int mouseY, float delta) { - super.render(drawContext, mouseX, mouseY, delta); - drawContext.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 10, 0xFFFFFF); - } - - - private void rebuildKeyboard() { - this.container = new SpruceContainerWidget(Position.of(0, this.height / 4 - 10), this.width, this.height / 2); - - - var row1 = new String[]{"q", "w", "e", "r", "t", "y", "u", "i", "o", "p"}; - var row2 = new String[]{"a", "s", "d", "f", "g", "h", "j", "k", "l"}; - var row3 = new String[]{"z", "x", "c", "v", "b", "n", "m"}; - - addKeyRow(0, row1); - addKeyRow(1, row2); - addKeyRow(2, row3); - } - - private void addKeyRow(int rowOffset, String... keys) { - int keyWidth = 20; - int spacing = 2; - int totalWidth = (keyWidth + spacing) * keys.length - spacing; - int startX = (this.width - totalWidth) / 2; - int y = this.height / 4 + rowOffset * 24; - - for (int i = 0; i < keys.length; i++) { - String key = keys[i]; - this.container.addChild(new SpruceButtonWidget(Position.of(startX + i * (keyWidth + spacing), y), keyWidth, 20, Text.literal(key), btn -> handleKeyPress(key))); - } - } - - private void handleKeyPress(String key) { - if (this.client == null) return; - - // TODO - if (key.equals("\b")) { - if (!buffer.isEmpty()) { - buffer.deleteCharAt(buffer.length() - 1); - } - } else { - buffer.append(key); - } - - if (this.bufferDisplay != null) { - this.bufferDisplay.setText(buffer.toString()); - } - } - - @Override - public boolean shouldPause() { - return false; - } - - @Override - public void close() { - super.close(); - if (closeCallback != null) { - closeCallback.onClose(buffer.toString()); - } - } -} \ No newline at end of file diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/virtualkeyboard/KeyInfo.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/virtualkeyboard/KeyInfo.java new file mode 100644 index 0000000..513a5e4 --- /dev/null +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/virtualkeyboard/KeyInfo.java @@ -0,0 +1,16 @@ +package eu.midnightdust.midnightcontrols.client.gui.virtualkeyboard; + +public record KeyInfo(String keySymbol, String displayText, double widthFactor) { + // Convenience constructor for standard width keys + KeyInfo(String keySymbol, double widthFactor) { + this(keySymbol, keySymbol, widthFactor); + } + + KeyInfo(String keySymbol) { + this(keySymbol, 1.0); + } + + KeyInfo(String keySymbol, String displayText) { + this(keySymbol, displayText, 1.0); + } +} diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/virtualkeyboard/KeyboardLayout.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/virtualkeyboard/KeyboardLayout.java new file mode 100644 index 0000000..ccaa4d3 --- /dev/null +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/virtualkeyboard/KeyboardLayout.java @@ -0,0 +1,60 @@ +package eu.midnightdust.midnightcontrols.client.gui.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( + new KeyInfo("q"), new KeyInfo("w"), new KeyInfo("e"), new KeyInfo("r"), new KeyInfo("t"), + new KeyInfo("y"), new KeyInfo("u"), new KeyInfo("i"), new KeyInfo("o"), new KeyInfo("p") + )); + letters.add(Arrays.asList( + new KeyInfo("a"), new KeyInfo("s"), new KeyInfo("d"), new KeyInfo("f"), new KeyInfo("g"), + new KeyInfo("h"), new KeyInfo("j"), new KeyInfo("k"), new KeyInfo("l") + )); + letters.add(Arrays.asList( + new KeyInfo("z"), new KeyInfo("x"), new KeyInfo("c"), new KeyInfo("v"), + new KeyInfo("b"), new KeyInfo("n"), new KeyInfo("m") + )); + return letters; + } + + private static List> createSymbolLayout() { + List> symbols = new ArrayList<>(); + symbols.add(Arrays.asList( + new KeyInfo("1"), new KeyInfo("2"), new KeyInfo("3"), new KeyInfo("4"), new KeyInfo("5"), + new KeyInfo("6"), new KeyInfo("7"), new KeyInfo("8"), new KeyInfo("9"), new KeyInfo("0") + )); + symbols.add(Arrays.asList( + new KeyInfo("@"), new KeyInfo("#"), new KeyInfo("$"), new KeyInfo("%"), new KeyInfo("&"), + new KeyInfo("*"), new KeyInfo("-"), new KeyInfo("+"), new KeyInfo("("), new KeyInfo(")") + )); + symbols.add(Arrays.asList( + new KeyInfo("!"), new KeyInfo("\""), new KeyInfo("'"), new KeyInfo(":"), new KeyInfo(";"), + new KeyInfo(","), new KeyInfo("."), new KeyInfo("?"), new KeyInfo("/") + )); + return symbols; + } +} \ No newline at end of file diff --git a/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/virtualkeyboard/VirtualKeyboardScreen.java b/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/virtualkeyboard/VirtualKeyboardScreen.java new file mode 100644 index 0000000..758089b --- /dev/null +++ b/common/src/main/java/eu/midnightdust/midnightcontrols/client/gui/virtualkeyboard/VirtualKeyboardScreen.java @@ -0,0 +1,308 @@ +package eu.midnightdust.midnightcontrols.client.gui.virtualkeyboard; + +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; + public 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); + drawContext.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 15, 0xFFFFFF); + 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 keys = getActiveKeyLayout(); + var keyboardContainer = createKeyboardContainer(keys); + + addLetterRows(keyboardContainer, keys); + 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 ? 3 : 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 addLetterRows(SpruceContainerWidget container, List> keyRows) { + int currentY = CONTAINER_PADDING; + + for (List row : keyRows) { + int rowWidth = calculateRowWidth(row); + // center row + int currentX = (container.getWidth() - rowWidth) / 2; + + for (KeyInfo keyInfo : row) { + int keyWidth = (int) (STANDARD_KEY_WIDTH * keyInfo.widthFactor()); + String displayText = getKeyDisplayText(keyInfo); + container.addChild( + new SpruceButtonWidget( + Position.of(currentX, currentY), + keyWidth, + KEY_HEIGHT, + Text.literal(displayText), + btn -> handleKeyPress(displayText) + ) + ); + + currentX += keyWidth + HORIZONTAL_SPACING; + } + + currentY += KEY_HEIGHT + VERTICAL_SPACING; + } + } + + private int calculateRowWidth(List row) { + int rowWidth = 0; + for (int i = 0; i < row.size(); i++) { + rowWidth += (int) (STANDARD_KEY_WIDTH * row.get(i).widthFactor()); + 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 String getKeyDisplayText(KeyInfo keyInfo) { + if(this.capsMode && !this.symbolMode) { + return keyInfo.displayText().toUpperCase(); + } + + return keyInfo.displayText(); + } + + 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/fabric/src/main/java/eu/midnightdust/midnightcontrols/fabric/event/MouseClickListener.java b/fabric/src/main/java/eu/midnightdust/midnightcontrols/fabric/event/MouseClickListener.java index d8b3610..5980a85 100644 --- a/fabric/src/main/java/eu/midnightdust/midnightcontrols/fabric/event/MouseClickListener.java +++ b/fabric/src/main/java/eu/midnightdust/midnightcontrols/fabric/event/MouseClickListener.java @@ -1,10 +1,9 @@ package eu.midnightdust.midnightcontrols.fabric.event; -import eu.midnightdust.midnightcontrols.client.gui.VirtualKeyboardScreen; +import eu.midnightdust.midnightcontrols.client.gui.virtualkeyboard.VirtualKeyboardScreen; import eu.midnightdust.midnightcontrols.client.mixin.AbstractSignEditScreenAccessor; import eu.midnightdust.midnightcontrols.client.mixin.BookEditScreenAccessor; import eu.midnightdust.midnightcontrols.client.mixin.CreativeInventoryScreenAccessor; import net.fabricmc.fabric.api.client.screen.v1.ScreenMouseEvents; -import net.minecraft.block.entity.SignText; import net.minecraft.client.gui.Element; import net.minecraft.client.gui.ParentElement; import net.minecraft.client.gui.screen.ChatScreen; @@ -18,7 +17,6 @@ import org.lwjgl.glfw.GLFW; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; import static eu.midnightdust.midnightcontrols.MidnightControls.logger; import static eu.midnightdust.midnightcontrols.client.MidnightControlsClient.client; @@ -89,14 +87,14 @@ public class MouseClickListener implements ScreenMouseEvents.AllowMouseClick { virtualKeyboardScreen = new VirtualKeyboardScreen(accessor.midnightcontrols$getTitle(), (text) -> { client.setScreen(bookEditScreen); accessor.midnightcontrols$setTitle(text); - }); + }, true); } else { virtualKeyboardScreen = new VirtualKeyboardScreen(accessor.midnightcontrols$getCurrentPageContent(), (text) -> { client.setScreen(bookEditScreen); accessor.midnightcontrols$setPageContent(text); accessor.midnightcontrols$getCurrentPageSelectionManager().putCursorAtEnd(); - }); + }, true); } client.setScreen(virtualKeyboardScreen); @@ -109,7 +107,7 @@ public class MouseClickListener implements ScreenMouseEvents.AllowMouseClick { private void handleTextFieldClick(TextFieldWidget textField) { this.link = new ScreenLink(screen, calculatePathToElement(screen, textField)); - var virtualKeyboardScreen = new VirtualKeyboardScreen(textField.getText(), this::handleKeyboardClose); + var virtualKeyboardScreen = new VirtualKeyboardScreen(textField.getText(), this::handleKeyboardClose, false); client.setScreen(virtualKeyboardScreen); } @@ -126,7 +124,6 @@ public class MouseClickListener implements ScreenMouseEvents.AllowMouseClick { txtField.setText(newText); - switch (this.link.screen()) { case CreativeInventoryScreen creativeInventoryScreen -> { var accessor = (CreativeInventoryScreenAccessor) creativeInventoryScreen;