feat: data-driven virtual keyboard layouts

This commit is contained in:
Martin Prokoph
2025-05-19 16:20:46 +02:00
parent ecb7cfd888
commit 0dfd1994dc
13 changed files with 122 additions and 59 deletions

View File

@@ -148,6 +148,7 @@ public class MidnightControlsConfig extends MidnightConfig {
@Comment(category = SCREENS, centered = true, name="\uD83D\uDD27 UI Modifications") public static Comment _uiMods; @Comment(category = SCREENS, centered = true, name="\uD83D\uDD27 UI Modifications") public static Comment _uiMods;
@Entry(category = SCREENS, name = "midnightcontrols.menu.move_chat") public static boolean moveChat = false; @Entry(category = SCREENS, name = "midnightcontrols.menu.move_chat") public static boolean moveChat = false;
@Entry(category = SCREENS, name = "Enable Shortcut in Controls Options") public static boolean shortcutInControls = true; @Entry(category = SCREENS, name = "Enable Shortcut in Controls Options") public static boolean shortcutInControls = true;
@Entry(category = MISC, name = "midnightcontrols.menu.virtual_keyboard_layout") public static String keyboardLayout = "en_US:qwerty";
@Entry(category = MISC, name = "Debug") public static boolean debug = false; @Entry(category = MISC, name = "Debug") public static boolean debug = false;
@Entry(category = MISC, name = "Excluded Keybindings") public static List<String> excludedKeybindings = Lists.newArrayList("key.forward", "key.left", "key.back", "key.right", "key.jump", "key.sneak", "key.sprint", "key.inventory", @Entry(category = MISC, name = "Excluded Keybindings") public static List<String> excludedKeybindings = Lists.newArrayList("key.forward", "key.left", "key.back", "key.right", "key.jump", "key.sneak", "key.sprint", "key.inventory",
"key.swapOffhand", "key.drop", "key.use", "key.attack", "key.chat", "key.playerlist", "key.screenshot", "key.togglePerspective", "key.smoothCamera", "key.fullscreen", "key.saveToolbarActivator", "key.loadToolbarActivator", "key.swapOffhand", "key.drop", "key.use", "key.attack", "key.chat", "key.playerlist", "key.screenshot", "key.togglePerspective", "key.smoothCamera", "key.fullscreen", "key.saveToolbarActivator", "key.loadToolbarActivator",

View File

@@ -0,0 +1,16 @@
package eu.midnightdust.midnightcontrols.client;
import eu.midnightdust.midnightcontrols.client.virtualkeyboard.KeyboardLayoutManager;
import net.minecraft.resource.ResourceManager;
import net.minecraft.resource.SynchronousResourceReloader;
public class MidnightControlsReloadListener implements SynchronousResourceReloader {
public static final MidnightControlsReloadListener INSTANCE = new MidnightControlsReloadListener();
private MidnightControlsReloadListener() {}
@Override
public void reload(ResourceManager manager) {
manager.findResources("keyboard_layouts", path -> path.toString().startsWith("midnightcontrols") && path.toString().endsWith(".json")).forEach(KeyboardLayoutManager::loadLayout);
}
}

View File

@@ -8,35 +8,32 @@ import java.util.List;
public class KeyboardLayout { public class KeyboardLayout {
public static KeyboardLayout QWERTY = new KeyboardLayout("US (Qwerty)", "en-US", createQwertyLetterLayout(), createSymbolLayout()); public static KeyboardLayout QWERTY = new KeyboardLayout("en_US:qwerty", createQwertyLetterLayout(), createSymbolLayout());
public static final List<KeyboardLayout> KEYBOARD_LAYOUTS = new ArrayList<>();
private final String name; private final String id;
private final String locale;
private final List<List<String>> letters; private final List<List<String>> letters;
private final List<List<String>> symbols; private final List<List<String>> symbols;
private KeyboardLayout(String name, String locale, List<List<String>> letters, List<List<String>> symbols) { private KeyboardLayout(String id, List<List<String>> letters, List<List<String>> symbols) {
this.name = name; this.id = id;
this.locale = locale;
this.letters = letters; this.letters = letters;
this.symbols = symbols; this.symbols = symbols;
} }
public KeyboardLayout fromJson(JsonObject json) { public static KeyboardLayout fromJson(JsonObject json) {
try { try {
return new KeyboardLayout(json.get("metadata").getAsJsonObject().get("name").getAsString(), json.get("metadata").getAsJsonObject().get("locale").getAsString(), getFromJson(json, true), getFromJson(json, false)); return new KeyboardLayout(json.get("id").getAsString(), getFromJson(json, true), getFromJson(json, false));
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Error loading keyboard definition: %s".formatted(e)); throw new RuntimeException("Error loading keyboard definition: %s".formatted(e));
} }
} }
public List<List<String>> getFromJson(JsonObject json, boolean letters) { private static List<List<String>> getFromJson(JsonObject json, boolean letters) {
String type = letters ? "letters" : "symbols"; String type = letters ? "letters" : "symbols";
List<List<String>> arr = new ArrayList<>(); List<List<String>> arr = new ArrayList<>();
if (json.has(type)) { if (json.has(type)) {
JsonObject lettersJson = json.get(type).getAsJsonObject(); JsonObject lettersJson = json.get(type).getAsJsonObject();
for (int i = 0; ; i++) { for (int i = 0; ; i++) {
if (!lettersJson.has("row%s".formatted(i))) break; if (!lettersJson.has("row"+i)) break;
var rowJson = lettersJson.get("row%s".formatted(i)).getAsJsonArray(); var rowJson = lettersJson.get("row%s".formatted(i)).getAsJsonArray();
List<String> row = new ArrayList<>(); List<String> row = new ArrayList<>();
for (int j = 0; j < rowJson.size(); j++) { for (int j = 0; j < rowJson.size(); j++) {
@@ -51,12 +48,8 @@ public class KeyboardLayout {
} }
} }
public String getName() { public String getId() {
return name; return id;
}
public String getLocale() {
return locale;
} }
public List<List<String>> getLetters() { public List<List<String>> getLetters() {

View File

@@ -0,0 +1,27 @@
package eu.midnightdust.midnightcontrols.client.virtualkeyboard;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import eu.midnightdust.midnightcontrols.client.MidnightControlsConfig;
import net.minecraft.resource.Resource;
import net.minecraft.util.Identifier;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class KeyboardLayoutManager {
private static final Map<String, KeyboardLayout> KEYBOARD_LAYOUTS = new HashMap<>();
public static void loadLayout(Identifier id, Resource resource) {
try {
JsonObject json = JsonParser.parseReader(resource.getReader()).getAsJsonObject();
KeyboardLayout layout = KeyboardLayout.fromJson(json);
KEYBOARD_LAYOUTS.put(layout.getId(), layout);
if (MidnightControlsConfig.debug) System.out.printf("Loaded keyboard layout: %s\n", layout.getId());
} catch (IOException e) { throw new RuntimeException(e); }
}
public static KeyboardLayout getById(String id) {
return KEYBOARD_LAYOUTS.get(id) == null ? KeyboardLayout.QWERTY : KEYBOARD_LAYOUTS.get(id);
}
}

View File

@@ -1,6 +1,8 @@
package eu.midnightdust.midnightcontrols.client.virtualkeyboard.gui; package eu.midnightdust.midnightcontrols.client.virtualkeyboard.gui;
import eu.midnightdust.midnightcontrols.client.MidnightControlsConfig;
import eu.midnightdust.midnightcontrols.client.virtualkeyboard.KeyboardLayout; import eu.midnightdust.midnightcontrols.client.virtualkeyboard.KeyboardLayout;
import eu.midnightdust.midnightcontrols.client.virtualkeyboard.KeyboardLayoutManager;
import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.DrawContext;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import org.thinkingstudio.obsidianui.Position; import org.thinkingstudio.obsidianui.Position;
@@ -42,10 +44,10 @@ public class VirtualKeyboardScreen extends SpruceScreen {
private SpruceContainerWidget keyboardContainer; private SpruceContainerWidget keyboardContainer;
public VirtualKeyboardScreen(String initialText, CloseCallback closeCallback, boolean newLineSupport) { public VirtualKeyboardScreen(String initialText, CloseCallback closeCallback, boolean newLineSupport) {
super(Text.literal("Virtual Keyboard")); super(Text.translatable("midnightcontrols.virtual_keyboard.screen"));
this.buffer = new StringBuilder(initialText); this.buffer = new StringBuilder(initialText);
this.closeCallback = closeCallback; this.closeCallback = closeCallback;
this.layout = KeyboardLayout.QWERTY; this.layout = KeyboardLayoutManager.getById(MidnightControlsConfig.keyboardLayout);
this.capsMode = false; this.capsMode = false;
this.symbolMode = false; this.symbolMode = false;
this.newLineSupport = newLineSupport; this.newLineSupport = newLineSupport;
@@ -187,7 +189,7 @@ public class VirtualKeyboardScreen extends SpruceScreen {
} }
private void addFunctionKeys(SpruceContainerWidget container) { private void addFunctionKeys(SpruceContainerWidget container) {
List<String> firstRow = getActiveKeyLayout().get(0); List<String> firstRow = getActiveKeyLayout().getFirst();
int firstRowWidth = calculateRowWidth(firstRow); int firstRowWidth = calculateRowWidth(firstRow);
// position backspace at the right of the first row // position backspace at the right of the first row
@@ -239,7 +241,7 @@ public class VirtualKeyboardScreen extends SpruceScreen {
Position.of(spaceX, rowY), Position.of(spaceX, rowY),
spaceKeyWidth, spaceKeyWidth,
KEY_HEIGHT, KEY_HEIGHT,
Text.literal("Space"), Text.translatable("midnightcontrols.virtual_keyboard.keyboard.space"),
btn -> handleKeyPress(SPACE_SYMBOL) btn -> handleKeyPress(SPACE_SYMBOL)
) )
); );

View File

@@ -1,16 +0,0 @@
{
"metadata": {
"name": "German (Quertz)",
"locale": "de-DE"
},
"letters": {
"row1": ["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "ü"],
"row2": ["a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä"],
"row3": ["y", "x", "c", "v", "b", "n", "m", "ß"]
},
"symbols": {
"row1": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
"row2": ["@", "#", "$", "%", "&", "*", "-", "+", "(", ")"],
"row3": ["!", "\"", "'", ":", ";", ",", ".", "?", "/"]
}
}

View File

@@ -1,16 +0,0 @@
{
"metadata": {
"name": "US (Querty)",
"locale": "en-US"
},
"letters": {
"row1": ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
"row2": ["a", "s", "d", "f", "g", "h", "j", "k", "l"],
"row3": ["z", "x", "c", "v", "b", "n", "m"]
},
"symbols": {
"row1": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
"row2": ["@", "#", "$", "%", "&", "*", "-", "+", "(", ")"],
"row3": ["!", "\"", "'", ":", ";", ",", ".", "?", "/"]
}
}

View File

@@ -0,0 +1,14 @@
{
"id": "de_DE:qwertz",
"letters": {
"row0": ["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "ü"],
"row1": ["a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä"],
"row2": ["y", "x", "c", "v", "b", "n", "m", "ß"]
},
"symbols": {
"row0": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
"row1": ["@", "#", "$", "%", "&", "*", "-", "+", "(", ")"],
"row2": ["!", "\"", "'", ":", ";", ",", ".", "?", "/"]
}
}

View File

@@ -0,0 +1,14 @@
{
"id": "en_US:qwerty",
"letters": {
"row0": ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
"row1": ["a", "s", "d", "f", "g", "h", "j", "k", "l"],
"row2": ["z", "x", "c", "v", "b", "n", "m"]
},
"symbols": {
"row0": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
"row1": ["@", "#", "$", "%", "&", "*", "-", "+", "(", ")"],
"row2": ["!", "\"", "'", ":", ";", ",", ".", "?", "/"]
}
}

View File

@@ -146,6 +146,10 @@
"midnightcontrols.menu.title.visual": "Visuelle Optionen", "midnightcontrols.menu.title.visual": "Visuelle Optionen",
"midnightcontrols.menu.unfocused_input": "Unfokussierte Eingabe", "midnightcontrols.menu.unfocused_input": "Unfokussierte Eingabe",
"midnightcontrols.menu.unfocused_input.tooltip": "Erlaube Controllereingabe auch wenn das Fenster nicht fokussiert ist.", "midnightcontrols.menu.unfocused_input.tooltip": "Erlaube Controllereingabe auch wenn das Fenster nicht fokussiert ist.",
"midnightcontrols.virtual_keyboard.screen": "Virtuelle Tastatur",
"midnightcontrols.virtual_keyboard.keyboard.space": "Leertaste",
"midnightcontrols.virtual_keyboard.layout.en_US.qwerty": "Englisch (Qwerty)",
"midnightcontrols.virtual_keyboard.layout.de_DE.qwertz": "Deutsch (Qwertz)",
"midnightcontrols.menu.virtual_mouse": "Virtuelle Maus", "midnightcontrols.menu.virtual_mouse": "Virtuelle Maus",
"midnightcontrols.menu.virtual_mouse.tooltip": "Aktiviere die virtuelle Maus.", "midnightcontrols.menu.virtual_mouse.tooltip": "Aktiviere die virtuelle Maus.",
"midnightcontrols.menu.virtual_mouse.skin": "Aussehen der Virtuellen Maus", "midnightcontrols.menu.virtual_mouse.skin": "Aussehen der Virtuellen Maus",

View File

@@ -217,15 +217,21 @@
"midnightcontrols.menu.touch_with_controller": "Touch in Controller mode", "midnightcontrols.menu.touch_with_controller": "Touch in Controller mode",
"midnightcontrols.menu.unfocused_input": "Unfocused Input", "midnightcontrols.menu.unfocused_input": "Unfocused Input",
"midnightcontrols.menu.unfocused_input.tooltip": "Allows controller input when the window is not focused.", "midnightcontrols.menu.unfocused_input.tooltip": "Allows controller input when the window is not focused.",
"midnightcontrols.menu.virtual_keyboard": "Virtual Keyboard",
"midnightcontrols.menu.virtual_keyboard.tooltip": "Enables a virtual on-screen keyboard",
"midnightcontrols.menu.virtual_keyboard_layout": "Virtual Keyboard Layout",
"midnightcontrols.menu.virtual_keyboard_layout.tooltip": "Defines which layout the on-screen keyboard will follow.",
"midnightcontrols.menu.virtual_mouse": "Virtual Mouse", "midnightcontrols.menu.virtual_mouse": "Virtual Mouse",
"midnightcontrols.menu.virtual_mouse.tooltip": "Enables the virtual mouse, which is useful during splitscreen.", "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_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": "Hide Normal Mouse Cursor",
"midnightcontrols.menu.hide_cursor.tooltip": "Hides the normal mouse cursor, leaving only the virtual mouse visible.", "midnightcontrols.menu.hide_cursor.tooltip": "Hides the normal mouse cursor, leaving only the virtual mouse visible.",
"midnightcontrols.narrator.unbound": "Unbound %s", "midnightcontrols.narrator.unbound": "Unbound %s",
"midnightcontrols.not_bound": "Not bound", "midnightcontrols.not_bound": "Not bound",
"midnightcontrols.virtual_keyboard.screen": "Virtual Keyboard",
"midnightcontrols.virtual_keyboard.keyboard.space": "Space",
"midnightcontrols.virtual_keyboard.layout.en_US.qwerty": "English (Qwerty)",
"midnightcontrols.virtual_keyboard.layout.de_DE.qwertz": "German (Qwertz)",
"midnightcontrols.virtual_mouse.skin.default_light": "Default Light", "midnightcontrols.virtual_mouse.skin.default_light": "Default Light",
"midnightcontrols.virtual_mouse.skin.default_dark": "Default Dark", "midnightcontrols.virtual_mouse.skin.default_dark": "Default Dark",
"midnightcontrols.virtual_mouse.skin.second_light": "Second Light", "midnightcontrols.virtual_mouse.skin.second_light": "Second Light",

View File

@@ -3,6 +3,7 @@ package eu.midnightdust.midnightcontrols.fabric;
import eu.midnightdust.midnightcontrols.MidnightControlsConstants; import eu.midnightdust.midnightcontrols.MidnightControlsConstants;
import eu.midnightdust.midnightcontrols.client.MidnightControlsClient; import eu.midnightdust.midnightcontrols.client.MidnightControlsClient;
import eu.midnightdust.midnightcontrols.client.MidnightControlsConfig; import eu.midnightdust.midnightcontrols.client.MidnightControlsConfig;
import eu.midnightdust.midnightcontrols.client.MidnightControlsReloadListener;
import eu.midnightdust.midnightcontrols.fabric.event.MouseClickListener; import eu.midnightdust.midnightcontrols.fabric.event.MouseClickListener;
import eu.midnightdust.midnightcontrols.packet.ControlsModePayload; import eu.midnightdust.midnightcontrols.packet.ControlsModePayload;
import eu.midnightdust.midnightcontrols.packet.FeaturePayload; import eu.midnightdust.midnightcontrols.packet.FeaturePayload;
@@ -16,8 +17,12 @@ import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
import net.fabricmc.fabric.api.client.screen.v1.ScreenMouseEvents; import net.fabricmc.fabric.api.client.screen.v1.ScreenMouseEvents;
import net.fabricmc.fabric.api.resource.ResourceManagerHelper; import net.fabricmc.fabric.api.resource.ResourceManagerHelper;
import net.fabricmc.fabric.api.resource.ResourcePackActivationType; import net.fabricmc.fabric.api.resource.ResourcePackActivationType;
import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener;
import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer; import net.fabricmc.loader.api.ModContainer;
import net.minecraft.resource.ResourceManager;
import net.minecraft.resource.ResourceType;
import net.minecraft.util.Identifier;
import java.util.Optional; import java.util.Optional;
@@ -62,5 +67,16 @@ public class MidnightControlsClientFabric implements ClientModInitializer {
ResourceManagerHelper.registerBuiltinResourcePack(id("legacy"), modContainer, ResourcePackActivationType.NORMAL); ResourceManagerHelper.registerBuiltinResourcePack(id("legacy"), modContainer, ResourcePackActivationType.NORMAL);
}); });
MidnightControlsClient.initClient(); MidnightControlsClient.initClient();
ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES).registerReloadListener(new SimpleSynchronousResourceReloadListener() {
@Override
public Identifier getFabricId() {
return id("keyboard_layouts");
}
@Override
public void reload(ResourceManager manager) {
MidnightControlsReloadListener.INSTANCE.reload(manager);
}
});
} }
} }

View File

@@ -2,6 +2,7 @@ package eu.midnightdust.midnightcontrols.neoforge;
import eu.midnightdust.midnightcontrols.client.MidnightControlsClient; import eu.midnightdust.midnightcontrols.client.MidnightControlsClient;
import eu.midnightdust.midnightcontrols.client.MidnightControlsConfig; import eu.midnightdust.midnightcontrols.client.MidnightControlsConfig;
import eu.midnightdust.midnightcontrols.client.MidnightControlsReloadListener;
import eu.midnightdust.midnightcontrols.client.util.platform.NetworkUtil; import eu.midnightdust.midnightcontrols.client.util.platform.NetworkUtil;
import eu.midnightdust.midnightcontrols.packet.ControlsModePayload; import eu.midnightdust.midnightcontrols.packet.ControlsModePayload;
import eu.midnightdust.midnightcontrols.packet.HelloPayload; import eu.midnightdust.midnightcontrols.packet.HelloPayload;
@@ -19,10 +20,7 @@ import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.ModList; import net.neoforged.fml.ModList;
import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.fml.common.Mod; import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; import net.neoforged.neoforge.client.event.*;
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.neoforge.event.AddPackFindersEvent;
import net.neoforged.neoforgespi.locating.IModFile; import net.neoforged.neoforgespi.locating.IModFile;
@@ -75,6 +73,10 @@ public class MidnightControlsClientNeoforge {
} catch (NullPointerException e) {e.fillInStackTrace();} } catch (NullPointerException e) {e.fillInStackTrace();}
})); }));
} }
@SubscribeEvent
public static void onResourceReload(AddClientReloadListenersEvent event) {
event.addListener(id("keyboard-layouts"), MidnightControlsReloadListener.INSTANCE);
}
} }
@EventBusSubscriber(modid = NAMESPACE, bus = EventBusSubscriber.Bus.GAME, value = Dist.CLIENT) @EventBusSubscriber(modid = NAMESPACE, bus = EventBusSubscriber.Bus.GAME, value = Dist.CLIENT)