[config-ui-tiny] preliminary json editing support

This commit is contained in:
Johannes Frohnmeyer 2023-03-14 18:25:49 +01:00
parent df10f1bf47
commit 01481bb4df
Signed by: Johannes
GPG Key ID: E76429612C2929F4
5 changed files with 571 additions and 4 deletions

View File

@ -4,5 +4,6 @@
"libjf-config-v1.see-also": "See also: %s",
"libjf-config-v1.reset": "Reset",
"libjf-config-core-v1.no-screen": "No screen",
"libjf-config-core-v1.no-screen.description": "No mod for rendering config UIs was discovered. Install LibJF to add one."
"libjf-config-core-v1.no-screen.description": "No mod for rendering config UIs was discovered. Install LibJF to add one.",
"libjf-config-core-v1.edit": "Edit"
}

View File

@ -0,0 +1,505 @@
package io.gitlab.jfronny.libjf.config.impl.ui.tiny;
import com.google.common.collect.Lists;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
import io.gitlab.jfronny.commons.ref.R;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import net.minecraft.SharedConstants;
import net.minecraft.client.font.TextHandler;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawableHelper;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.ingame.BookScreen;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.util.NarratorManager;
import net.minecraft.client.util.SelectionManager;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.client.util.math.Rect2i;
import net.minecraft.screen.ScreenTexts;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.util.Util;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.mutable.MutableBoolean;
import org.apache.commons.lang3.mutable.MutableInt;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Consumer;
public class EditorScreen extends Screen {
//TODO simple box background
//TODO scrollable
private static final int MAX_TEXT_WIDTH = 114;
private static final int MAX_TEXT_HEIGHT = 128;
private static final int WIDTH = 192;
private static final int HEIGHT = 192;
private int tickCounter;
private final String initialText;
private String text = "";
private final SelectionManager currentPageSelectionManager = new SelectionManager(
this::getText,
this::setText,
this::getClipboard,
this::setClipboard,
string -> string.length() < 1024 && this.textRenderer.getWrappedLinesHeight(string, MAX_TEXT_WIDTH) <= MAX_TEXT_HEIGHT
);
private long lastClickTime;
private int lastClickIndex = -1;
@Nullable
private PageContent pageContent = PageContent.EMPTY;
private final Screen parent;
private final Consumer<String> onSave;
public EditorScreen(@Nullable Text title, @Nullable Screen parent, @Nullable String text, @Nullable Consumer<String> onSave) {
super(title == null ? NarratorManager.EMPTY : title);
this.parent = parent;
this.initialText = text;
if (text != null) this.text = text;
this.onSave = onSave == null ? R::nop : onSave;
}
public void setText(String text) {
this.text = Objects.requireNonNull(text);
}
public String getText() {
return this.text;
}
private void setClipboard(String clipboard) {
if (this.client != null) {
SelectionManager.setClipboard(this.client, clipboard);
}
}
private String getClipboard() {
return this.client != null ? SelectionManager.getClipboard(this.client) : "";
}
@Override
public void tick() {
super.tick();
++this.tickCounter;
}
@Override
protected void init() {
ButtonWidget cancelButton;
ButtonWidget doneButton;
this.invalidatePageContent();
doneButton = this.addDrawableChild(
ButtonWidget.builder(ScreenTexts.DONE, button -> quit(true))
.dimensions(this.width / 2 + 2, HEIGHT + 4, 98, 20)
.build()
);
cancelButton = this.addDrawableChild(
ButtonWidget.builder(ScreenTexts.CANCEL, button -> quit(false))
.dimensions(this.width / 2 - 102, HEIGHT + 4, 98, 20)
.build()
);
}
private void quit(boolean save) {
if (save && (this.initialText == null || !this.initialText.equals(this.text))) onSave.accept(text);
close();
}
@Override
public void close() {
Objects.requireNonNull(client).setScreen(parent);
}
@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (super.keyPressed(keyCode, scanCode, modifiers)) {
return true;
}
boolean bl = this.keyPressedEditMode(keyCode);
if (bl) {
this.invalidatePageContent();
return true;
}
return false;
}
@Override
public boolean charTyped(char chr, int modifiers) {
if (super.charTyped(chr, modifiers)) {
return true;
}
if (SharedConstants.isValidChar(chr)) {
this.currentPageSelectionManager.insert(Character.toString(chr));
this.invalidatePageContent();
return true;
}
return false;
}
private boolean keyPressedEditMode(int keyCode) {
boolean ctrl = Screen.hasControlDown() && !Screen.hasShiftDown() && !Screen.hasAltDown();
SelectionManager.SelectionType selectionType = Screen.hasControlDown() ? SelectionManager.SelectionType.WORD : SelectionManager.SelectionType.CHARACTER;
return switch (keyCode) {
case 65 -> {
if (ctrl) {
this.currentPageSelectionManager.selectAll();
yield true;
}
yield false;
}
case 67 -> {
if (ctrl) {
this.currentPageSelectionManager.copy();
yield true;
}
yield false;
}
case 83 -> {
if (ctrl) {
quit(true);
yield true;
}
yield false;
}
case 86 -> {
if (ctrl) {
this.currentPageSelectionManager.paste();
yield true;
}
yield false;
}
case 88 -> {
if (ctrl) {
this.currentPageSelectionManager.cut();
yield true;
}
yield false;
}
case 259 -> {
this.currentPageSelectionManager.delete(-1, selectionType);
yield true;
}
case 261 -> {
this.currentPageSelectionManager.delete(1, selectionType);
yield true;
}
case 257, 335 -> {
this.currentPageSelectionManager.insert("\n");
yield true;
}
case 263 -> {
this.currentPageSelectionManager.moveCursor(-1, Screen.hasShiftDown(), selectionType);
yield true;
}
case 262 -> {
this.currentPageSelectionManager.moveCursor(1, Screen.hasShiftDown(), selectionType);
yield true;
}
case 265 -> {
this.moveUpLine();
yield true;
}
case 264 -> {
this.moveDownLine();
yield true;
}
case 268 -> {
this.moveToLineStart();
yield true;
}
case 269 -> {
this.moveToLineEnd();
yield true;
}
default -> false;
};
}
private void moveUpLine() {
this.moveVertically(-1);
}
private void moveDownLine() {
this.moveVertically(1);
}
private void moveVertically(int lines) {
int i = this.currentPageSelectionManager.getSelectionStart();
int j = this.getPageContent().getVerticalOffset(i, lines);
this.currentPageSelectionManager.moveCursorTo(j, Screen.hasShiftDown());
}
private void moveToLineStart() {
if (Screen.hasControlDown()) {
this.currentPageSelectionManager.moveCursorToStart(Screen.hasShiftDown());
} else {
int i = this.currentPageSelectionManager.getSelectionStart();
int j = this.getPageContent().getLineStart(i);
this.currentPageSelectionManager.moveCursorTo(j, Screen.hasShiftDown());
}
}
private void moveToLineEnd() {
if (Screen.hasControlDown()) {
this.currentPageSelectionManager.moveCursorToEnd(Screen.hasShiftDown());
} else {
int i = this.currentPageSelectionManager.getSelectionStart();
int j = this.getPageContent().getLineEnd(i);
this.currentPageSelectionManager.moveCursorTo(j, Screen.hasShiftDown());
}
}
@Override
public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
this.renderBackground(matrices);
this.setFocused(null);
RenderSystem.setShaderTexture(0, BookScreen.BOOK_TEXTURE);
int i = (this.width - WIDTH) / 2;
drawTexture(matrices, i, 2, 0, 0, WIDTH, HEIGHT);
PageContent pageContent = this.getPageContent();
for (Line line : pageContent.lines) {
this.textRenderer.draw(matrices, line.text, line.x, line.y, -16777216);
}
this.drawSelection(matrices, pageContent.selectionRectangles);
this.drawCursor(matrices, pageContent.position, pageContent.atEnd);
super.render(matrices, mouseX, mouseY, delta);
}
private void drawCursor(MatrixStack matrices, Position position, boolean atEnd) {
if (this.tickCounter / 6 % 2 == 0) {
position = this.absolutePositionToScreenPosition(position);
if (!atEnd) {
DrawableHelper.fill(matrices, position.x, position.y - 1, position.x + 1, position.y + this.textRenderer.fontHeight, -16777216);
} else {
this.textRenderer.draw(matrices, "_", position.x, position.y, 0);
}
}
}
private void drawSelection(MatrixStack matrices, Rect2i[] selectionRectangles) {
RenderSystem.enableColorLogicOp();
RenderSystem.logicOp(GlStateManager.LogicOp.OR_REVERSE);
for (Rect2i rect2i : selectionRectangles) {
int i = rect2i.getX();
int j = rect2i.getY();
int k = i + rect2i.getWidth();
int l = j + rect2i.getHeight();
fill(matrices, i, j, k, l, -16776961);
}
RenderSystem.disableColorLogicOp();
}
private Position screenPositionToAbsolutePosition(Position position) {
return new Position(position.x - (this.width - WIDTH) / 2 - 36, position.y - 32);
}
private Position absolutePositionToScreenPosition(Position position) {
return new Position(position.x + (this.width - WIDTH) / 2 + 36, position.y + 32);
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (super.mouseClicked(mouseX, mouseY, button)) {
return true;
}
if (button == 0) {
long l = Util.getMeasuringTimeMs();
int i = this.getPageContent().getCursorPosition(this.textRenderer, this.screenPositionToAbsolutePosition(new Position((int)mouseX, (int)mouseY)));
if (i >= 0) {
if (i == this.lastClickIndex && l - this.lastClickTime < 250L) {
if (!this.currentPageSelectionManager.isSelecting()) {
this.selectCurrentWord(i);
} else {
this.currentPageSelectionManager.selectAll();
}
} else {
this.currentPageSelectionManager.moveCursorTo(i, Screen.hasShiftDown());
}
this.invalidatePageContent();
}
this.lastClickIndex = i;
this.lastClickTime = l;
}
return true;
}
private void selectCurrentWord(int cursor) {
String string = this.getText();
this.currentPageSelectionManager.setSelection(TextHandler.moveCursorByWords(string, -1, cursor, false), TextHandler.moveCursorByWords(string, 1, cursor, false));
}
@Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
if (super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) {
return true;
}
if (button == 0) {
int i = this.getPageContent().getCursorPosition(this.textRenderer, this.screenPositionToAbsolutePosition(new Position((int)mouseX, (int)mouseY)));
this.currentPageSelectionManager.moveCursorTo(i, true);
this.invalidatePageContent();
}
return true;
}
private PageContent getPageContent() {
if (this.pageContent == null) {
this.pageContent = this.createPageContent();
}
return this.pageContent;
}
private void invalidatePageContent() {
this.pageContent = null;
}
private PageContent createPageContent() {
String content = this.getText();
if (content.isEmpty()) {
return PageContent.EMPTY;
}
int i = this.currentPageSelectionManager.getSelectionStart();
int j = this.currentPageSelectionManager.getSelectionEnd();
IntArrayList intList = new IntArrayList();
ArrayList<Line> list = Lists.newArrayList();
MutableInt lineIndexM = new MutableInt();
MutableBoolean mutableBoolean = new MutableBoolean();
TextHandler textHandler = this.textRenderer.getTextHandler();
textHandler.wrapLines(content, MAX_TEXT_WIDTH, Style.EMPTY, true, (style, start, end) -> {
int lineIndex = lineIndexM.getAndIncrement();
String string = content.substring(start, end);
mutableBoolean.setValue(string.endsWith("\n"));
String string2 = StringUtils.stripEnd(string, " \n");
int y = lineIndex * this.textRenderer.fontHeight;
Position position = this.absolutePositionToScreenPosition(new Position(0, y));
intList.add(start);
list.add(new Line(style, string2, position.x, position.y));
});
int[] is = intList.toIntArray();
boolean bl = i == content.length();
int l;
Position position;
if (bl && mutableBoolean.isTrue()) {
position = new Position(0, list.size() * this.textRenderer.fontHeight);
} else {
int k = getLineFromOffset(is, i);
l = this.textRenderer.getWidth(content.substring(is[k], i));
position = new Position(l, k * this.textRenderer.fontHeight);
}
ArrayList<Rect2i> list2 = Lists.newArrayList();
if (i != j) {
int o;
l = Math.min(i, j);
int m = Math.max(i, j);
int n = getLineFromOffset(is, l);
if (n == (o = getLineFromOffset(is, m))) {
int p = n * this.textRenderer.fontHeight;
int q = is[n];
list2.add(this.getLineSelectionRectangle(content, textHandler, l, m, p, q));
} else {
int p = n + 1 > is.length ? content.length() : is[n + 1];
list2.add(this.getLineSelectionRectangle(content, textHandler, l, p, n * this.textRenderer.fontHeight, is[n]));
for (int q = n + 1; q < o; ++q) {
int r = q * this.textRenderer.fontHeight;
String string2 = content.substring(is[q], is[q + 1]);
int s = (int)textHandler.getWidth(string2);
list2.add(this.getRectFromCorners(new Position(0, r), new Position(s, r + this.textRenderer.fontHeight)));
}
list2.add(this.getLineSelectionRectangle(content, textHandler, is[o], m, o * this.textRenderer.fontHeight, is[o]));
}
}
return new PageContent(content, position, bl, is, list.toArray(new Line[0]), list2.toArray(new Rect2i[0]));
}
static int getLineFromOffset(int[] lineStarts, int position) {
int i = Arrays.binarySearch(lineStarts, position);
if (i < 0) {
return -(i + 2);
}
return i;
}
private Rect2i getLineSelectionRectangle(String string, TextHandler handler, int selectionStart, int selectionEnd, int lineY, int lineStart) {
String string2 = string.substring(lineStart, selectionStart);
String string3 = string.substring(lineStart, selectionEnd);
Position position = new Position((int)handler.getWidth(string2), lineY);
Position position2 = new Position((int)handler.getWidth(string3), lineY + this.textRenderer.fontHeight);
return this.getRectFromCorners(position, position2);
}
private Rect2i getRectFromCorners(Position start, Position end) {
Position position = this.absolutePositionToScreenPosition(start);
Position position2 = this.absolutePositionToScreenPosition(end);
int i = Math.min(position.x, position2.x);
int j = Math.max(position.x, position2.x);
int k = Math.min(position.y, position2.y);
int l = Math.max(position.y, position2.y);
return new Rect2i(i, k, j - i, l - k);
}
static class PageContent {
static final PageContent EMPTY = new PageContent("", new Position(0, 0), true, new int[]{0}, new Line[]{new Line(Style.EMPTY, "", 0, 0)}, new Rect2i[0]);
private final String pageContent;
final Position position;
final boolean atEnd;
private final int[] lineStarts;
final Line[] lines;
final Rect2i[] selectionRectangles;
public PageContent(String pageContent, Position position, boolean atEnd, int[] lineStarts, Line[] lines, Rect2i[] selectionRectangles) {
this.pageContent = pageContent;
this.position = position;
this.atEnd = atEnd;
this.lineStarts = lineStarts;
this.lines = lines;
this.selectionRectangles = selectionRectangles;
}
public int getCursorPosition(TextRenderer renderer, Position position) {
int i = position.y / renderer.fontHeight;
if (i < 0) {
return 0;
}
if (i >= this.lines.length) {
return this.pageContent.length();
}
Line line = this.lines[i];
return this.lineStarts[i] + renderer.getTextHandler().getTrimmedLength(line.content, position.x, line.style);
}
public int getVerticalOffset(int position, int lines) {
int m;
int i = getLineFromOffset(this.lineStarts, position);
int j = i + lines;
if (0 <= j && j < this.lineStarts.length) {
int k = position - this.lineStarts[i];
int l = this.lines[j].content.length();
m = this.lineStarts[j] + Math.min(k, l);
} else {
m = position;
}
return m;
}
public int getLineStart(int position) {
int i = getLineFromOffset(this.lineStarts, position);
return this.lineStarts[i];
}
public int getLineEnd(int position) {
int i = getLineFromOffset(this.lineStarts, position);
return this.lineStarts[i] + this.lines[i].content.length();
}
}
record Line(Style style, String content, Text text, int x, int y) {
public Line(Style style, String content, int x, int y) {
this(style, content, Text.literal(content).setStyle(style), x, y);
}
}
record Position(int x, int y) { }
}

View File

@ -16,6 +16,7 @@ public class WidgetState<T> {
public Text error;
public boolean inLimits = true;
public String tempValue;
public boolean managedTemp = true;
public T cachedValue;
@Nullable public WidgetFactory factory;
@ -37,7 +38,7 @@ public class WidgetState<T> {
public void updateCache(T newValue) {
cachedValue = newValue;
tempValue = newValue == null ? null : newValue.toString();
if (managedTemp) tempValue = newValue == null ? null : newValue.toString();
}
public void writeToEntry() throws IllegalAccessException {

View File

@ -1,19 +1,26 @@
package io.gitlab.jfronny.libjf.config.impl.ui.tiny.entry;
import io.gitlab.jfronny.commons.ref.R;
import io.gitlab.jfronny.commons.serialize.gson.api.v1.GsonHolders;
import io.gitlab.jfronny.commons.throwable.Try;
import io.gitlab.jfronny.gson.JsonElement;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigCategory;
import io.gitlab.jfronny.libjf.config.api.v1.EntryInfo;
import io.gitlab.jfronny.libjf.config.api.v1.type.Type;
import io.gitlab.jfronny.libjf.config.api.v1.ui.tiny.WidgetFactory;
import io.gitlab.jfronny.libjf.config.impl.ui.tiny.EditorScreen;
import io.gitlab.jfronny.libjf.config.impl.ui.tiny.WidgetState;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.client.toast.SystemToast;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import java.io.StringWriter;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Function;
@ -75,8 +82,8 @@ public class EntryInfoWidgetBuilder {
}
});
} else {
LibJf.LOGGER.error("Unsupported entry type in " + info.getName() + ": " + type.getName() + " - not displaying config control");
factory = null;
LibJf.LOGGER.error("Unsupported entry type in " + info.getName() + ": " + type.getName() + " - displaying fallback");
factory = jsonScreen(config, info, state);
}
Try.orThrow(() -> state.initialize(info, knownStates, factory, config.getTranslationPrefix()));
@ -147,6 +154,55 @@ public class EntryInfoWidgetBuilder {
};
}
private static <T> WidgetFactory jsonScreen(ConfigCategory config, EntryInfo<T> info, WidgetState<T> state) {
state.managedTemp = false;
state.tempValue = null;
return (screen, textRenderer) -> new WidgetFactory.Widget(
R::nop,
ButtonWidget.builder(Text.translatable("libjf-config-core-v1.edit"), button -> {
final String jsonified;
if (state.tempValue == null) {
try {
jsonified = GsonHolders.CONFIG.getGson().toJson(state.cachedValue);
} catch (Throwable e) {
LibJf.LOGGER.error("Could not stringify element", e);
SystemToast.add(
screen.getClient().getToastManager(),
SystemToast.Type.PACK_LOAD_FAILURE,
Text.translatable("libjf-config-ui-tiny-v1.entry.json.read.fail.title"),
Text.translatable("libjf-config-ui-tiny-v1.entry.json.read.fail.description")
);
return;
}
} else {
jsonified = state.tempValue;
}
screen.getClient().setScreen(new EditorScreen(
Text.translatable(config.getTranslationPrefix() + info.getName()),
screen,
jsonified,
json -> {
try {
state.updateCache(GsonHolders.CONFIG.getGson().fromJson(json, info.getValueType().asClass()));
state.tempValue = null;
} catch (Throwable e) {
LibJf.LOGGER.error("Could not write element", e);
SystemToast.add(
screen.getClient().getToastManager(),
SystemToast.Type.PACK_LOAD_FAILURE,
Text.translatable("libjf-config-ui-tiny-v1.entry.json.write.fail.title"),
Text.translatable("libjf-config-ui-tiny-v1.entry.json.write.fail.description")
);
state.tempValue = json;
}
}
));
})
.dimensions(screen.width - 110, 0, info.getWidth(), 20)
.build()
);
}
private static <T extends Number> WidgetFactory slider(EntryInfo info, WidgetState state, Function<T, Double> t2d, Function<Double, T> d2t, boolean wholeNumber) {
double min = info.getMinValue();
double max = info.getMaxValue();

View File

@ -0,0 +1,4 @@
{
"libjf-config-ui-tiny-v1.entry.json.read.fail.title": "Could not read",
"libjf-config-ui-tiny-v1.entry.json.read.fail.description": "The given entry could not be stringified. Please edit the config manually"
}