Finalize virtual keyboard screen

- Added backspace support
- Added space support
- Added new line support
- Added caps support
- Added symbol support
This commit is contained in:
cryy
2025-04-22 19:36:26 +02:00
parent 50103ce4cf
commit 662bac3053
5 changed files with 388 additions and 115 deletions

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<List<KeyInfo>> letters;
private final List<List<KeyInfo>> symbols;
private KeyboardLayout(List<List<KeyInfo>> letters, List<List<KeyInfo>> symbols) {
this.letters = letters;
this.symbols = symbols;
}
public List<List<KeyInfo>> getLetters() {
return letters;
}
public List<List<KeyInfo>> getSymbols() {
return symbols;
}
private static List<List<KeyInfo>> createQwertyLetterLayout() {
List<List<KeyInfo>> 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<List<KeyInfo>> createSymbolLayout() {
List<List<KeyInfo>> 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;
}
}

View File

@@ -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<List<KeyInfo>> 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<List<KeyInfo>> 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<List<KeyInfo>> keyRows) {
int currentY = CONTAINER_PADDING;
for (List<KeyInfo> 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<KeyInfo> 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<KeyInfo> 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<KeyInfo> 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<List<KeyInfo>> 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();
}
}

View File

@@ -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;