Compare commits

...

24 Commits

Author SHA1 Message Date
Martin Prokoph
54a43d41c0 chore: bump version 2025-05-19 23:31:20 +02:00
Martin Prokoph
c86df8714c fix: crash on NeoForge 1.21.5 2025-05-19 23:30:06 +02:00
Martin Prokoph
ccad5e1d44 clean: remove GlStateManager calls 2025-05-19 23:14:17 +02:00
Martin Prokoph
58fa3b5bc1 Merge pull request #350 from TeamMidnightDust/feat/virtual-keyboard
feat: virtual keyboard support!
2025-05-19 23:12:39 +02:00
Martin Prokoph
0f407ac245 feat: edit signs using the virtual keyboard! 2025-05-19 22:49:26 +02:00
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
Martin Prokoph
18e69c1cb3 Merge pull request #343 from cryy/feat/virtual-keyboard
Feat: Virtual keyboard support
2025-05-08 09:53:58 +02:00
Martin Prokoph
f5be8eb14e Merge branch 'feat/virtual-keyboard' into feat/virtual-keyboard 2025-05-08 09:53:33 +02:00
cryy
1231c231a9 Fix virtual keyboard config 2025-04-22 23:57:53 +02:00
cryy
24e58027b2 Add obisidianUI text field interop 2025-04-22 23:57:31 +02:00
cryy
33845e111b Fix handler naming 2025-04-22 23:08:48 +02:00
cryy
86622962f8 Fix tooltip entry in en_us locale 2025-04-22 23:05:02 +02:00
cryy
73c5fe1a82 Add virtual keyboard config option 2025-04-22 23:03:07 +02:00
cryy
70d923f959 Implement NeoForge support 2025-04-22 22:52:57 +02:00
cryy
0ef59057af Add missing row width calculation 2025-04-22 20:21:02 +02:00
cryy
9ebd1a9cea Increase displayed lines for newline support 2025-04-22 20:17:25 +02:00
cryy
bb5c6976c0 Replace KeyInfo with String
- KeyInfo was leftover code from toying around with various implementations. String is sufficient in current implementation.
2025-04-22 20:16:08 +02:00
cryy
041eeb29aa Make special key constant private 2025-04-22 19:39:56 +02:00
cryy
662bac3053 Finalize virtual keyboard screen
- Added backspace support
- Added space support
- Added new line support
- Added caps support
- Added symbol support
2025-04-22 19:36:26 +02:00
cryy
50103ce4cf Move cursor to end of text in book 2025-04-20 19:53:26 +02:00
cryy
9e12381471 Implement basic virtual keyboard support
- Listener for clicks inside of text fields and other text-based screens
- Virtual keyboard screen in a QWERTY layout
2025-04-18 19:43:01 +02:00
29 changed files with 1044 additions and 23 deletions

View File

@@ -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;
@@ -71,15 +72,17 @@ public class MidnightControlsClient extends MidnightControls {
public static final Identifier WAYLAND_CURSOR_TEXTURE_LIGHT = id("cursor/light/mouse_pointer");
public static final Identifier WAYLAND_CURSOR_TEXTURE_DARK = id("cursor/dark/mouse_pointer");
public static final File MAPPINGS_FILE = new File("config/gamecontrollercustommappings.txt");
public static final MinecraftClient client = MinecraftClient.getInstance();
public static MinecraftClient client = MinecraftClient.getInstance();
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;
public static void initClient() {
client = MinecraftClient.getInstance();
ring.registerAction("buttonbinding", ButtonBindingRingAction.FACTORY);
int delay = 0; // delay for 0 sec.
@@ -204,10 +207,12 @@ public class MidnightControlsClient extends MidnightControls {
RainbowColor.tick();
TouchInput.tick();
}
/**
* Called when opening a screen.
*/
public static void onScreenOpen(Screen screen) {
client = MinecraftClient.getInstance();
if (screen == null && MidnightControlsConfig.controlsMode == ControlsMode.TOUCHSCREEN) {
screen = new TouchscreenOverlay();
screen.init(client, client.getWindow().getScaledWidth(), client.getWindow().getScaledHeight());

View File

@@ -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;
@@ -147,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",
@@ -402,6 +404,7 @@ public class MidnightControlsConfig extends MidnightConfig {
unfocusedInput = false;
virtualMouse = false;
virtualMouseSkin = VirtualMouseSkin.DEFAULT_LIGHT;
virtualKeyboard = false;
controllerID = 0;
secondControllerID = -1;
controllerType = ControllerType.DEFAULT;

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

@@ -10,7 +10,6 @@
package eu.midnightdust.midnightcontrols.client.gui;
import eu.midnightdust.midnightcontrols.ControlsMode;
import eu.midnightdust.midnightcontrols.MidnightControlsConstants;
import eu.midnightdust.midnightcontrols.client.enums.HudSide;
import eu.midnightdust.midnightcontrols.client.MidnightControlsClient;
import eu.midnightdust.midnightcontrols.client.MidnightControlsConfig;
@@ -24,7 +23,6 @@ import net.minecraft.client.resource.language.I18n;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.item.BlockItem;
import net.minecraft.item.ItemStack;
import net.minecraft.util.Identifier;
import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.hit.HitResult;
import org.jetbrains.annotations.NotNull;
@@ -40,7 +38,7 @@ import static eu.midnightdust.midnightcontrols.MidnightControls.id;
* @since 1.0.0
*/
public class MidnightControlsHud extends Hud {
private final MinecraftClient client = MinecraftClient.getInstance();
private MinecraftClient client = MinecraftClient.getInstance();
private int attackWidth = 0;
private int attackButtonWidth = 0;
private int dropItemWidth = 0;
@@ -63,6 +61,7 @@ public class MidnightControlsHud extends Hud {
@Override
public void init(@NotNull MinecraftClient client, int screenWidth, int screenHeight) {
this.client = MinecraftClient.getInstance();
super.init(client, screenWidth, screenHeight);
this.inventoryWidth = this.width(ButtonBinding.INVENTORY);
this.inventoryButtonWidth = MidnightControlsRenderer.getBindingIconWidth(ButtonBinding.INVENTORY);

View File

@@ -9,8 +9,6 @@
package eu.midnightdust.midnightcontrols.client.gui;
import com.mojang.blaze3d.opengl.GlStateManager;
import com.mojang.blaze3d.platform.GLX;
import com.mojang.blaze3d.systems.RenderSystem;
import eu.midnightdust.midnightcontrols.ControlsMode;
import eu.midnightdust.midnightcontrols.client.enums.ControllerType;
@@ -171,8 +169,6 @@ public class MidnightControlsRenderer {
case GLFW.GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER + 100, GLFW.GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER + 200 -> buttonOffset = 10 * 15;
}
GlStateManager._disableDepthTest();
int assetSize = axis || (button >= 15 && button <= 18) ? AXIS_SIZE : BUTTON_SIZE;
RenderSystem.setShaderColor(1.f, second ? 0.f : 1.f, 1.f, 1.f);
@@ -181,7 +177,6 @@ public class MidnightControlsRenderer {
(float) buttonOffset, (float) (controllerType * assetSize),
assetSize, assetSize,
256, 256);
GlStateManager._enableDepthTest();
return ICON_SIZE;
}

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;
@@ -65,6 +66,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;
@@ -139,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,
@@ -300,6 +311,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 +404,8 @@ public class MidnightControlsSettingsScreen extends SpruceScreen {
list.addSingleOptionEntry(this.yAxisRotationSpeedOption);
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);
@@ -513,13 +528,10 @@ public class MidnightControlsSettingsScreen extends SpruceScreen {
float g = (float)(color.getGreen()) / 255.0F;
float b = (float)(color.getBlue()) / 255.0F;
float t = (float)(transparency) / 255.0F;
GlStateManager._enableBlend();
//GlStateManager._defaultBlendFunc();
vertexConsumer.vertex((float)x1, (float)y2, 0.0F).color(r, g, b, t);
vertexConsumer.vertex((float)x2, (float)y2, 0.0F).color(r, g, b, t);
vertexConsumer.vertex((float)x2, (float)y1, 0.0F).color(r, g, b, t);
vertexConsumer.vertex((float)x1, (float)y1, 0.0F).color(r, g, b, t);
GlStateManager._disableBlend();
context.draw();
}
}

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

@@ -0,0 +1,34 @@
package eu.midnightdust.midnightcontrols.client.mixin;
import eu.midnightdust.midnightcontrols.client.util.AbstractSignEditScreenAccessor;
import net.minecraft.block.entity.SignBlockEntity;
import net.minecraft.block.entity.SignText;
import net.minecraft.client.gui.screen.ingame.AbstractSignEditScreen;
import net.minecraft.text.Text;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(AbstractSignEditScreen.class)
public class AbstractSignEditScreenMixin implements AbstractSignEditScreenAccessor {
@Shadow @Final private String[] messages;
@Shadow private SignText text;
@Shadow @Final protected SignBlockEntity blockEntity;
@Shadow @Final private boolean front;
@Override
public String[] midnightcontrols$getMessages() {
return messages;
}
@Override
public void midnightcontrols$setMessage(int line, String text) {
this.messages[line] = text;
this.text = this.text.withMessage(line, Text.literal(text));
}
@Override
public void midnightcontrols$writeToBlockEntity() {
this.blockEntity.setText(this.text, this.front);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
package eu.midnightdust.midnightcontrols.client.util;
import org.spongepowered.asm.mixin.Unique;
public interface AbstractSignEditScreenAccessor {
@Unique
String[] midnightcontrols$getMessages();
@Unique
void midnightcontrols$setMessage(int line, String text);
@Unique
void midnightcontrols$writeToBlockEntity();
}

View File

@@ -0,0 +1,102 @@
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("en_US:qwerty", createQwertyLetterLayout(), createSymbolLayout());
private final String id;
private final List<List<String>> letters;
private final 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;
}
public List<List<String>> getSymbols() {
return symbols;
}
private static List<List<String>> createQwertyLetterLayout() {
List<List<String>> 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<List<String>> createSymbolLayout() {
List<List<String>> 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;
}
}

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

@@ -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<Class<?>, 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 <T extends Screen> void intercept(T screen, double mouseX, double mouseY) {
AbstractScreenClickHandler<T> handler = (AbstractScreenClickHandler<T>) clickHandlers.get(screen.getClass());
if (handler == null) {
handler = (AbstractScreenClickHandler<T>) clickHandlers.get(Screen.class);
}
handler.handle(screen, mouseX, mouseY);
}
}

View File

@@ -0,0 +1,7 @@
package eu.midnightdust.midnightcontrols.client.virtualkeyboard.clickhandler;
import net.minecraft.client.gui.screen.Screen;
public abstract class AbstractScreenClickHandler<T extends Screen> {
public abstract void handle(T screen, double mouseX, double mouseY);
}

View File

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

View File

@@ -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<Screen> {
private Screen parentScreen;
private List<Integer> 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<? extends Element> 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<Integer> calculatePathToElement(Element parent, Element target) {
if (!(parent instanceof ParentElement parentElement)) {
return null;
}
List<? extends Element> 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<Integer> subPath = calculatePathToElement(child, target);
if (subPath != null) {
List<Integer> fullPath = new ArrayList<>(subPath.size() + 1);
fullPath.add(i);
fullPath.addAll(subPath);
return fullPath;
}
}
}
return null;
}
protected TextFieldWrapper findTextFieldByPath(Element parent, List<Integer> path) {
if (path == null || path.isEmpty()) {
return null;
}
if (!(parent instanceof ParentElement parentElement)) {
return null;
}
List<? extends Element> 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;
}
}

View File

@@ -0,0 +1,36 @@
package eu.midnightdust.midnightcontrols.client.virtualkeyboard.clickhandler;
import eu.midnightdust.midnightcontrols.client.util.AbstractSignEditScreenAccessor;
import eu.midnightdust.midnightcontrols.client.virtualkeyboard.gui.VirtualKeyboardScreen;
import net.minecraft.client.gui.screen.ingame.SignEditScreen;
import static eu.midnightdust.midnightcontrols.client.MidnightControlsClient.client;
public class SignEditScreenClickHandler extends AbstractScreenClickHandler<SignEditScreen> {
@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;
StringBuilder linesToString = new StringBuilder();
String[] messages = accessor.midnightcontrols$getMessages();
for (int i = 0; i < Math.min(4, messages.length); i++) {
String line = messages[i];
linesToString.append(line);
if (!line.isEmpty() && i < 3) linesToString.append("\n");
}
VirtualKeyboardScreen virtualKeyboardScreen = new VirtualKeyboardScreen(linesToString.toString(), (text) -> {
client.setScreen(screen);
String[] lines = text.split("\n");
for (int i = 0; i < 4; i++) accessor.midnightcontrols$setMessage(i, lines.length > i ? lines[i] : "");
accessor.midnightcontrols$writeToBlockEntity();
}, true);
client.setScreen(virtualKeyboardScreen);
}
}

View File

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

View File

@@ -0,0 +1,302 @@
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;
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.translatable("midnightcontrols.virtual_keyboard.screen"));
this.buffer = new StringBuilder(initialText);
this.closeCallback = closeCallback;
this.layout = KeyboardLayoutManager.getById(MidnightControlsConfig.keyboardLayout);
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<List<String>> 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<List<String>> 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<List<String>> keyLayoutRows) {
int currentY = CONTAINER_PADDING;
for (List<String> 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<String> 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<String> firstRow = getActiveKeyLayout().getFirst();
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<String> 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.translatable("midnightcontrols.virtual_keyboard.keyboard.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<List<String>> 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

@@ -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,6 +217,10 @@
"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",
@@ -224,6 +228,10 @@
"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,7 +3,10 @@
"package": "eu.midnightdust.midnightcontrols.client.mixin",
"compatibilityLevel": "JAVA_21",
"client": [
"AbstractBlockAccessor",
"AbstractSignEditScreenMixin",
"AdvancementsScreenAccessor",
"BookEditScreenAccessor",
"ChatScreenMixin",
"ClickableWidgetAccessor",
"ClientPlayerEntityMixin",
@@ -24,8 +27,7 @@
"RecipeBookWidgetAccessor",
"ScreenMixin",
"TabNavigationWidgetAccessor",
"WorldRendererMixin",
"AbstractBlockAccessor"
"WorldRendererMixin"
],
"injectors": {
"defaultRequire": 1

View File

@@ -3,6 +3,8 @@ 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;
import eu.midnightdust.midnightcontrols.packet.HelloPayload;
@@ -11,11 +13,16 @@ 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.fabric.api.resource.SimpleSynchronousResourceReloadListener;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import org.thinkingstudio.obsidianui.fabric.event.OpenScreenCallback;
import net.minecraft.resource.ResourceManager;
import net.minecraft.resource.ResourceType;
import net.minecraft.util.Identifier;
import java.util.Optional;
@@ -51,11 +58,25 @@ 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);
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

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

View File

@@ -8,14 +8,14 @@ yarn_mappings=1.21.5+build.1
enabled_platforms=fabric,neoforge
archives_base_name=midnightcontrols
mod_version=1.10.6
mod_version=1.11.0
maven_group=eu.midnightdust
release_type=release
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,9 +2,11 @@ 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;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.resource.DirectoryResourcePack;
import net.minecraft.resource.ResourcePackInfo;
import net.minecraft.resource.ResourcePackPosition;
@@ -18,9 +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.*;
import net.neoforged.neoforge.event.AddPackFindersEvent;
import net.neoforged.neoforgespi.locating.IModFile;
@@ -34,6 +34,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 {
@@ -71,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)
@@ -90,5 +96,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);
}
}
}
}