Compare commits

...

4 Commits

Author SHA1 Message Date
Martin Prokoph
7375e5ad20 feat: add reset button icon 2025-05-19 20:28:06 +02:00
Martin Prokoph
7b723513ae feat: easy switching between virtual keyboard layouts 2025-05-19 19:18:18 +02:00
Martin Prokoph
0dfd1994dc feat: data-driven virtual keyboard layouts 2025-05-19 16:20:46 +02:00
Martin Prokoph
ecb7cfd888 feat: load virtual keyboard layouts from JSON 2025-05-10 10:38:54 +02:00
14 changed files with 194 additions and 16 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;
@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 = 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 = "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",

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

@@ -14,6 +14,7 @@ import com.mojang.blaze3d.systems.RenderSystem;
import eu.midnightdust.midnightcontrols.MidnightControlsConstants;
import eu.midnightdust.midnightcontrols.client.MidnightControlsClient;
import eu.midnightdust.midnightcontrols.client.util.platform.NetworkUtil;
import eu.midnightdust.midnightcontrols.client.virtualkeyboard.KeyboardLayoutManager;
import org.thinkingstudio.obsidianui.background.Background;
import org.thinkingstudio.obsidianui.mixin.DrawContextAccessor;
import org.thinkingstudio.obsidianui.widget.SpruceWidget;
@@ -140,6 +141,15 @@ public class MidnightControlsSettingsScreen extends SpruceScreen {
maxAnalogValueOption("midnightcontrols.menu.max_right_x_value", GLFW.GLFW_GAMEPAD_AXIS_RIGHT_X),
maxAnalogValueOption("midnightcontrols.menu.max_right_y_value", GLFW.GLFW_GAMEPAD_AXIS_RIGHT_Y)
};
// Controller options
public final static SpruceOption virtualKeyboardLayoutOption =
new SpruceCyclingOption("midnightcontrols.menu.virtual_keyboard_layout",
amount -> {
MidnightControlsConfig.keyboardLayout = KeyboardLayoutManager.getNext(KeyboardLayoutManager.getById(MidnightControlsConfig.keyboardLayout)).getId();
},
option -> {
return option.getDisplayText(Text.translatable(KeyboardLayoutManager.getById(MidnightControlsConfig.keyboardLayout).getTranslationKey()));
}, null);
private static SpruceOption maxAnalogValueOption(String key, int axis) {
return new SpruceDoubleOption(key, .25f, 1.f, 0.05f,
@@ -395,6 +405,7 @@ public class MidnightControlsSettingsScreen extends SpruceScreen {
list.addSingleOptionEntry(this.mouseSpeedOption);
list.addSingleOptionEntry(this.virtualMouseOption);
list.addSingleOptionEntry(this.virtualKeyboardOption);
list.addSingleOptionEntry(this.virtualKeyboardLayoutOption);
list.addSingleOptionEntry(this.hideCursorOption);
list.addSingleOptionEntry(this.joystickAsMouseOption);
list.addSingleOptionEntry(this.eyeTrackingAsMouseOption);

View File

@@ -14,11 +14,14 @@ import eu.midnightdust.midnightcontrols.client.MidnightControlsConfig;
import eu.midnightdust.midnightcontrols.client.controller.ButtonBinding;
import eu.midnightdust.midnightcontrols.client.controller.ButtonCategory;
import eu.midnightdust.midnightcontrols.client.controller.InputManager;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.util.Identifier;
import org.thinkingstudio.obsidianui.Position;
import org.thinkingstudio.obsidianui.SpruceTexts;
import org.thinkingstudio.obsidianui.navigation.NavigationDirection;
import org.thinkingstudio.obsidianui.navigation.NavigationUtils;
import org.thinkingstudio.obsidianui.widget.SpruceButtonWidget;
import org.thinkingstudio.obsidianui.widget.SpruceIconButtonWidget;
import org.thinkingstudio.obsidianui.widget.SpruceSeparatorWidget;
import org.thinkingstudio.obsidianui.widget.SpruceWidget;
import org.thinkingstudio.obsidianui.widget.container.SpruceEntryListWidget;
@@ -86,7 +89,7 @@ public class ControlsListWidget extends SpruceEntryListWidget<ControlsListWidget
super(parent);
this.binding = binding;
this.bindingName = I18n.translate(this.binding.getTranslationKey());
this.editButton = new ControllerButtonWidget(Position.of(this, parent.getWidth() / 2 - 8, 0), 110, this.binding, btn -> {
this.editButton = new ControllerButtonWidget(Position.of(this, parent.getWidth() / 2 - 8, 0), 120, this.binding, btn -> {
gui.focusedBinding = binding;
MidnightControlsClient.input.beginControlsInput(gui);
}) {
@@ -96,13 +99,24 @@ public class ControlsListWidget extends SpruceEntryListWidget<ControlsListWidget
}
};
this.children.add(editButton);
this.resetButton = new SpruceButtonWidget(Position.of(this,
this.resetButton = new SpruceIconButtonWidget(Position.of(this,
this.editButton.getPosition().getRelativeX() + this.editButton.getWidth() + 2, 0),
44, 20, Text.translatable("controls.reset"),
37, 20, Text.empty(),
btn -> MidnightControlsConfig.setButtonBinding(binding, binding.getDefaultButton())) {
protected Text getNarrationMessage() {
return Text.translatable("narrator.controls.reset", bindingName);
}
private final Identifier resetTexture = Identifier.of("midnightlib","icon/reset");
@Override
protected int renderIcon(DrawContext drawContext, int mouseX, int mouseY, float delta) {
int size = 12;
int x = this.getX() + this.getWidth() / 2 - size / 2;
int y = this.getY() + this.getHeight() / 2 - size / 2;
drawContext.drawGuiTexture(RenderLayer::getGuiTextured, resetTexture, x, y, size, size);
return 1;
}
};
this.children.add(this.resetButton);
this.unbindButton = new SpruceButtonWidget(Position.of(this,

View File

@@ -1,21 +1,63 @@
package eu.midnightdust.midnightcontrols.client.virtualkeyboard;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class KeyboardLayout {
public static KeyboardLayout QWERTY = new KeyboardLayout(createQwertyLetterLayout(), createSymbolLayout());
public static KeyboardLayout QWERTY = new KeyboardLayout("en_US:qwerty", createQwertyLetterLayout(), createSymbolLayout());
private final String id;
private final List<List<String>> letters;
private final List<List<String>> symbols;
private KeyboardLayout(List<List<String>> letters, List<List<String>> symbols) {
private KeyboardLayout(String id, List<List<String>> letters, List<List<String>> symbols) {
this.id = id;
this.letters = letters;
this.symbols = symbols;
}
public static KeyboardLayout fromJson(JsonObject json) {
try {
return new KeyboardLayout(json.get("id").getAsString(), getFromJson(json, true), getFromJson(json, false));
} catch (Exception e) {
throw new RuntimeException("Error loading keyboard definition: %s".formatted(e));
}
}
private static List<List<String>> getFromJson(JsonObject json, boolean letters) {
String type = letters ? "letters" : "symbols";
List<List<String>> arr = new ArrayList<>();
if (json.has(type)) {
JsonObject lettersJson = json.get(type).getAsJsonObject();
for (int i = 0; ; i++) {
if (!lettersJson.has("row"+i)) break;
var rowJson = lettersJson.get("row%s".formatted(i)).getAsJsonArray();
List<String> row = new ArrayList<>();
for (int j = 0; j < rowJson.size(); j++) {
row.add(rowJson.get(j).getAsString());
}
arr.add(row);
}
return arr;
}
else {
return letters ? createQwertyLetterLayout() : createSymbolLayout();
}
}
public String getId() {
return id;
}
public String getTranslationKey() {
String[] identifier = id.split(":");
if (identifier.length != 2) return "Invalid Keyboard ID: %s".formatted(id);
return "midnightcontrols.virtual_keyboard.layout.%s.%s".formatted(identifier[0], identifier[1]);
}
public List<List<String>> getLetters() {
return letters;
}

View File

@@ -0,0 +1,36 @@
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);
}
public static KeyboardLayout getNext(KeyboardLayout current) {
KeyboardLayout[] layouts = KEYBOARD_LAYOUTS.values().toArray(KeyboardLayout[]::new);
int currentIndex = -1;
for (int i = 0; i < layouts.length; i++) {
if (layouts[i] == current) currentIndex = i;
}
currentIndex = currentIndex+1 >= layouts.length ? 0 : currentIndex + 1;
return layouts[currentIndex];
}
}

View File

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

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.unfocused_input": "Unfokussierte Eingabe",
"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.tooltip": "Aktiviere die virtuelle 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.unfocused_input": "Unfocused Input",
"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.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",
"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_dark": "Default Dark",
"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.client.MidnightControlsClient;
import eu.midnightdust.midnightcontrols.client.MidnightControlsConfig;
import eu.midnightdust.midnightcontrols.client.MidnightControlsReloadListener;
import eu.midnightdust.midnightcontrols.fabric.event.MouseClickListener;
import eu.midnightdust.midnightcontrols.packet.ControlsModePayload;
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.resource.ResourceManagerHelper;
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.ModContainer;
import net.minecraft.resource.ResourceManager;
import net.minecraft.resource.ResourceType;
import net.minecraft.util.Identifier;
import java.util.Optional;
@@ -62,5 +67,16 @@ public class MidnightControlsClientFabric implements ClientModInitializer {
ResourceManagerHelper.registerBuiltinResourcePack(id("legacy"), modContainer, ResourcePackActivationType.NORMAL);
});
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

@@ -15,7 +15,7 @@ modrinth_id = bXX9h73M
curseforge_id = 621768
# Configure the IDs here after creating the projects on the websites
midnightlib_version=1.7.0+1.21.4
midnightlib_version=1.7.3+1.21.4
fabric_loader_version=0.16.10
fabric_api_version=0.119.5+1.21.5

View File

@@ -2,6 +2,7 @@ package eu.midnightdust.midnightcontrols.neoforge;
import eu.midnightdust.midnightcontrols.client.MidnightControlsClient;
import eu.midnightdust.midnightcontrols.client.MidnightControlsConfig;
import eu.midnightdust.midnightcontrols.client.MidnightControlsReloadListener;
import eu.midnightdust.midnightcontrols.client.util.platform.NetworkUtil;
import eu.midnightdust.midnightcontrols.packet.ControlsModePayload;
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.common.EventBusSubscriber;
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.client.event.*;
import net.neoforged.neoforge.event.AddPackFindersEvent;
import net.neoforged.neoforgespi.locating.IModFile;
@@ -75,6 +73,10 @@ public class MidnightControlsClientNeoforge {
} 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)