From 72bffb8acda5b608ca3d9efbc42cd5c99a0adb39 Mon Sep 17 00:00:00 2001 From: J4PC Date: Sun, 15 Feb 2026 18:39:29 +0100 Subject: [PATCH 1/9] It works again --- .../java/me/chrr/scribble/ScribbleConfig.java | 8 ++ .../scribble/book/TextOverflowHandler.java | 89 +++++++++++++ .../scribble/gui/edit/OverflowHandler.java | 42 ++++++ .../chrr/scribble/gui/edit/RichEditBox.java | 123 ++++++++++++++++-- .../scribble/history/HistoryListener.java | 11 +- .../history/command/OverflowCommand.java | 41 ++++++ .../history/command/PageDeleteCommand.java | 6 +- .../history/command/PageInsertCommand.java | 2 +- .../screen/ScribbleBookEditScreen.java | 77 ++++++++++- .../resources/assets/scribble/lang/en_us.json | 5 + 10 files changed, 386 insertions(+), 18 deletions(-) create mode 100644 src/main/java/me/chrr/scribble/book/TextOverflowHandler.java create mode 100644 src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java create mode 100644 src/main/java/me/chrr/scribble/history/command/OverflowCommand.java diff --git a/src/main/java/me/chrr/scribble/ScribbleConfig.java b/src/main/java/me/chrr/scribble/ScribbleConfig.java index ab40b34..7feb8f8 100644 --- a/src/main/java/me/chrr/scribble/ScribbleConfig.java +++ b/src/main/java/me/chrr/scribble/ScribbleConfig.java @@ -25,6 +25,8 @@ public class ScribbleConfig extends ReflectedConfig { public Value copyFormattingCodes = value(true); public Value editHistorySize = value(32) .range(8, 128, 1); + public Value overflowWhenTyping = value(false); + public Value pasteBehavior = value(PasteBehavior.DENY); @Header("miscellaneous") public Value openVanillaBookScreenOnShift = value(false); @@ -43,4 +45,10 @@ public enum ShowActionButtons { WHEN_EDITING, NEVER, } + + public enum PasteBehavior { + DENY, + FIT_PAGE, + OVERFLOW, + } } diff --git a/src/main/java/me/chrr/scribble/book/TextOverflowHandler.java b/src/main/java/me/chrr/scribble/book/TextOverflowHandler.java new file mode 100644 index 0000000..d46c34d --- /dev/null +++ b/src/main/java/me/chrr/scribble/book/TextOverflowHandler.java @@ -0,0 +1,89 @@ +package me.chrr.scribble.book; + +import me.chrr.scribble.history.HistoryListener; +import me.chrr.scribble.history.command.OverflowCommand; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.Font; +import net.minecraft.network.chat.Style; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +@NullMarked +public class TextOverflowHandler { + private static final int PAGE_WIDTH = 114; + private static final int LINE_LIMIT = 14; + + private final HistoryListener listener; + private final Font font; + private final Consumer onCommand; + + public TextOverflowHandler(HistoryListener listener, Font font, Consumer onCommand) { + this.listener = listener; + this.font = font; + this.onCommand = onCommand; + } + + public boolean insertWithOverflow(int page, RichText text, int cursor, RichText insert, + @Nullable ChatFormatting color, Set modifiers) { + int maxPages = 100 - listener.getTotalPages() + 1; + if (cursor != text.getLength() || maxPages < 1) return false; + + // Truncate insert to max pages × ~400 chars/page (conservative estimate) + insert = insert.subText(0, Math.min(insert.getLength(), maxPages * 400)); + + // Combine text + insert first, then split with word-wrapping + RichText combined = text.insert(cursor, insert); + + List pages = new ArrayList<>(); + RichText remaining = combined; + for (; remaining.getLength() > 0 && pages.size() < maxPages; ) { + int len = findFittingLength(remaining); + pages.add(remaining.subText(0, len)); + remaining = remaining.subText(len, remaining.getLength()); + } + + // Reject only if no overflow pages were created (at page limit with no room) + // Allow truncation when overflow pages were successfully added + if (remaining.getLength() > 0 && pages.size() == 1) { + return false; + } + + OverflowCommand cmd = new OverflowCommand(page, listener.getPageContent(page), pages); + cmd.execute(listener); + onCommand.accept(cmd); + return true; + } + + private int findFittingLength(RichText text) { + if (!wouldOverflow(text)) return text.getLength(); + + // Binary search for max chars that fit on one page + int lo = 1, hi = text.getLength(), best = 1; + while (lo <= hi) { + int mid = (lo + hi) / 2; + if (!wouldOverflow(text.subText(0, mid))) { best = mid; lo = mid + 1; } + else hi = mid - 1; + } + + // Find last space in the fitted text for word wrapping + String plain = text.subText(0, best).getPlainText(); + int lastNewline = plain.lastIndexOf('\n'); + int searchStart = Math.max(0, lastNewline); // Only look for space after last newline + int lastSpace = plain.lastIndexOf(' ', best - 1); + + // Only use space if it's on the last visual line (after last newline) + if (lastSpace > searchStart) { + return lastSpace + 1; + } + return best; + } + + private boolean wouldOverflow(RichText text) { + return font.getSplitter().splitLines(text, PAGE_WIDTH, Style.EMPTY).size() > LINE_LIMIT; + } +} diff --git a/src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java b/src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java new file mode 100644 index 0000000..0575ff3 --- /dev/null +++ b/src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java @@ -0,0 +1,42 @@ +package me.chrr.scribble.gui.edit; + +import me.chrr.scribble.book.RichText; +import net.minecraft.ChatFormatting; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Set; + +/** + * Interface for handling text overflow situations in the book editor. + * This is called when text operations would cause the page to overflow. + */ +@NullMarked +public interface OverflowHandler { + /** + * Called when typing or pasting would overflow the current page. + * + * @param currentText the current text on the page + * @param cursor the cursor position + * @param insert the text to insert + * @param color the current color + * @param modifiers the current modifiers + * @return true if the overflow was handled, false if the operation should be rejected + */ + boolean handleOverflow(RichText currentText, int cursor, RichText insert, + @Nullable ChatFormatting color, Set modifiers); + + /** + * Called when Enter is pressed at the end of a page. + * + * @return true if a new page was created + */ + boolean handleEnterAtEnd(); + + /** + * Called when Backspace is pressed on a completely empty page. + * + * @return true if the page was deleted + */ + boolean handleBackspaceOnEmpty(); +} diff --git a/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java b/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java index 81f76e4..5fcf936 100644 --- a/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java +++ b/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java @@ -2,11 +2,14 @@ import com.mojang.blaze3d.platform.cursor.CursorTypes; import com.mojang.datafixers.util.Pair; +import me.chrr.scribble.Scribble; +import me.chrr.scribble.ScribbleConfig; import me.chrr.scribble.book.RichText; import me.chrr.scribble.gui.TextArea; import me.chrr.scribble.history.command.Command; import me.chrr.scribble.history.command.EditCommand; import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.MultiLineEditBox; @@ -32,6 +35,7 @@ public class RichEditBox extends MultiLineEditBox implements TextArea { private final @Nullable Runnable onInvalidateFormat; private final @Nullable Consumer onHistoryPush; + private final @Nullable OverflowHandler overflowHandler; public @Nullable ChatFormatting color = ChatFormatting.BLACK; public Set modifiers = new HashSet<>(); @@ -39,11 +43,13 @@ public class RichEditBox extends MultiLineEditBox implements TextArea private RichEditBox(Font font, int x, int y, int width, int height, Component placeholder, Component message, int textColor, boolean textShadow, int cursorColor, boolean hasBackground, boolean hasOverlay, - @Nullable Runnable onInvalidateFormat, @Nullable Consumer onHistoryPush) { + @Nullable Runnable onInvalidateFormat, @Nullable Consumer onHistoryPush, + @Nullable OverflowHandler overflowHandler) { super(font, x, y, width, height, placeholder, message, textColor, textShadow, cursorColor, hasBackground, hasOverlay); this.onInvalidateFormat = onInvalidateFormat; this.onHistoryPush = onHistoryPush; + this.overflowHandler = overflowHandler; this.textField = new RichMultiLineTextField( font, width - this.totalInnerPadding(), @@ -197,6 +203,30 @@ protected void renderContents(GuiGraphics graphics, int mouseX, int mouseY, floa @Override public boolean charTyped(CharacterEvent event) { if (this.visible && this.isFocused() && event.isAllowedChatCharacter()) { + RichMultiLineTextField tf = this.getRichTextField(); + RichText currentText = tf.getRichText(); + int cursor = tf.cursor; + + // Create the text to insert + RichText insert = new RichText(event.codepointAsString(), + Optional.ofNullable(color).orElse(ChatFormatting.BLACK), modifiers); + + // Check if this would overflow + RichText result = tf.hasSelection() + ? currentText.replace(tf.getSelected().beginIndex(), tf.getSelected().endIndex(), insert) + : currentText.insert(cursor, insert); + + boolean wouldOverflow = tf.hasLineLimit() && tf.font.getSplitter() + .splitLines(result, tf.width, net.minecraft.network.chat.Style.EMPTY).size() > tf.lineLimit; + + // If would overflow and we have an overflow handler, try to handle it + if (wouldOverflow && this.overflowHandler != null && cursor == currentText.getLength() && !tf.hasSelection()) { + if (this.overflowHandler.handleOverflow(currentText, cursor, insert, color, modifiers)) { + return true; + } + } + + // Normal behavior EditCommand command = new EditCommand(this, (textField) -> textField.insertText(event.codepointAsString())); command.executeEdit(this.getRichTextField()); @@ -226,17 +256,84 @@ public boolean keyPressed(KeyEvent event) { } } - // Wrap the operation with an edit command if it edits the text. - if (event.isCut() || event.isPaste() || - List.of(GLFW.GLFW_KEY_ENTER, GLFW.GLFW_KEY_KP_ENTER, - GLFW.GLFW_KEY_BACKSPACE, GLFW.GLFW_KEY_DELETE).contains(event.key())) { - EditCommand command = new EditCommand(this, - (textField) -> textField.keyPressed(event)); - command.executeEdit(this.getRichTextField()); + RichMultiLineTextField tf = this.getRichTextField(); + RichText currentText = tf.getRichText(); + int cursor = tf.cursor; + boolean isAtEndOfText = cursor == currentText.getLength() && !tf.hasSelection(); + + // Handle Enter at end of full page - create new page if overflow handler exists + if ((event.key() == GLFW.GLFW_KEY_ENTER || event.key() == GLFW.GLFW_KEY_KP_ENTER) && isAtEndOfText) { + boolean pageFull = tf.hasLineLimit() && tf.font.getSplitter() + .splitLines(currentText, tf.width, net.minecraft.network.chat.Style.EMPTY).size() >= tf.lineLimit; + if (pageFull) { + if (this.overflowHandler != null && this.overflowHandler.handleEnterAtEnd()) { + return true; + } + return true; // Block enter on full page if overflow disabled + } + } + + // Handle Backspace on empty page - delete page if overflow handler exists + if (event.key() == GLFW.GLFW_KEY_BACKSPACE && currentText.isEmpty() && !tf.hasSelection()) { + if (this.overflowHandler != null && this.overflowHandler.handleBackspaceOnEmpty()) { + return true; + } + } + + // Handle Paste with different behaviors based on config + if (event.isPaste()) { + String clipboardText = Minecraft.getInstance().keyboardHandler.getClipboard(); + boolean keepFormatting = Scribble.config().copyFormattingCodes.get() ^ event.hasShiftDown(); + if (!keepFormatting) clipboardText = ChatFormatting.stripFormatting(clipboardText); + + RichText insert = ChatFormatting.stripFormatting(clipboardText).equals(clipboardText) + ? new RichText(clipboardText, Optional.ofNullable(color).orElse(ChatFormatting.BLACK), modifiers) + : RichText.fromFormattedString(clipboardText); + + int start = tf.hasSelection() ? tf.getSelected().beginIndex() : cursor; + int end = tf.hasSelection() ? tf.getSelected().endIndex() : cursor; + RichText result = tf.hasSelection() ? currentText.replace(start, end, insert) : currentText.insert(cursor, insert); + boolean wouldOverflow = tf.hasLineLimit() && tf.font.getSplitter() + .splitLines(result, tf.width, net.minecraft.network.chat.Style.EMPTY).size() > tf.lineLimit; + + if (wouldOverflow) { + ScribbleConfig.PasteBehavior behavior = Scribble.config().pasteBehavior.get(); + if (behavior == ScribbleConfig.PasteBehavior.FIT_PAGE) { + int lo = 0, hi = insert.getLength(), best = 0; + while (lo <= hi) { + int mid = (lo + hi) / 2; + RichText partial = insert.subText(0, mid); + RichText partialResult = tf.hasSelection() ? currentText.replace(start, end, partial) : currentText.insert(cursor, partial); + if (tf.font.getSplitter().splitLines(partialResult, tf.width, net.minecraft.network.chat.Style.EMPTY).size() <= tf.lineLimit) { + best = mid; lo = mid + 1; + } else hi = mid - 1; + } + if (best > 0) { + String truncated = insert.subText(0, best).getAsFormattedString(); + EditCommand cmd = new EditCommand(this, t -> t.insertText(truncated)); + cmd.executeEdit(tf); + this.pushHistory(cmd); + } + return true; + } else if (behavior == ScribbleConfig.PasteBehavior.OVERFLOW && this.overflowHandler != null && isAtEndOfText) { + if (this.overflowHandler.handleOverflow(currentText, cursor, insert, color, modifiers)) return true; + } + } + + EditCommand command = new EditCommand(this, t -> t.keyPressed(event)); + command.executeEdit(tf); this.pushHistory(command); return true; } + // Wrap the operation with an edit command if it edits the text. + if (event.isCut() || List.of(GLFW.GLFW_KEY_ENTER, GLFW.GLFW_KEY_KP_ENTER, + GLFW.GLFW_KEY_BACKSPACE, GLFW.GLFW_KEY_DELETE).contains(event.key())) { + EditCommand command = new EditCommand(this, t -> t.keyPressed(event)); + command.executeEdit(tf); + this.pushHistory(command); + return true; + } return super.keyPressed(event); } @@ -267,6 +364,8 @@ public static class Builder extends MultiLineEditBox.Builder { private Runnable onInvalidateFormat = null; @Nullable private Consumer onHistoryPush = null; + @Nullable + private OverflowHandler overflowHandler = null; public Builder onInvalidateFormat(Runnable onInvalidateFormat) { this.onInvalidateFormat = onInvalidateFormat; @@ -278,13 +377,19 @@ public Builder onHistoryPush(Consumer onHistoryPush) { return this; } + public Builder onOverflow(OverflowHandler overflowHandler) { + this.overflowHandler = overflowHandler; + return this; + } + @Override public MultiLineEditBox build(Font font, int width, int height, Component message) { return new RichEditBox(font, this.x, this.y, width, height, this.placeholder, message, this.textColor, this.textShadow, this.cursorColor, this.showBackground, - this.showDecorations, this.onInvalidateFormat, this.onHistoryPush); + this.showDecorations, this.onInvalidateFormat, this.onHistoryPush, + this.overflowHandler); } } } diff --git a/src/main/java/me/chrr/scribble/history/HistoryListener.java b/src/main/java/me/chrr/scribble/history/HistoryListener.java index c438fd3..927df3d 100644 --- a/src/main/java/me/chrr/scribble/history/HistoryListener.java +++ b/src/main/java/me/chrr/scribble/history/HistoryListener.java @@ -16,5 +16,14 @@ public interface HistoryListener { void insertPageAt(int page, @Nullable RichText content); - void deletePage(int page); + /** @param navigateDirection negative = go left, 0 or positive = stay/go right */ + void deletePage(int page, int navigateDirection); + + int getTotalPages(); + + RichText getPageContent(int page); + + void setPageContent(int page, RichText content); + + void refreshPages(); } diff --git a/src/main/java/me/chrr/scribble/history/command/OverflowCommand.java b/src/main/java/me/chrr/scribble/history/command/OverflowCommand.java new file mode 100644 index 0000000..c596252 --- /dev/null +++ b/src/main/java/me/chrr/scribble/history/command/OverflowCommand.java @@ -0,0 +1,41 @@ +package me.chrr.scribble.history.command; + +import me.chrr.scribble.book.RichText; +import me.chrr.scribble.gui.edit.RichMultiLineTextField; +import me.chrr.scribble.history.HistoryListener; + +import java.util.List; + +public class OverflowCommand implements Command { + private final int page; + private final RichText before; + private final List after; + + public OverflowCommand(int page, RichText before, List after) { + this.page = page; + this.before = before; + this.after = List.copyOf(after); + } + + @Override + public void execute(HistoryListener listener) { + int newPages = after.size() - 1; + for (int i = 0; i < newPages; i++) listener.insertPageAt(page + 1 + i, null); + for (int i = 0; i < after.size(); i++) listener.setPageContent(page + i, after.get(i)); + listener.refreshPages(); + setCursor(listener.switchAndFocusPage(page + newPages), after.get(newPages).getLength()); + } + + @Override + public void rollback(HistoryListener listener) { + for (int i = after.size() - 1; i > 0; i--) listener.deletePage(page + i, 0); + listener.setPageContent(page, before); + listener.refreshPages(); + setCursor(listener.switchAndFocusPage(page), before.getLength()); + } + + private void setCursor(RichMultiLineTextField tf, int pos) { + tf.cursor = tf.selectCursor = pos; + tf.onValueChange(); + } +} diff --git a/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java b/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java index ff17bab..07edcc6 100644 --- a/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java +++ b/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java @@ -8,15 +8,17 @@ public class PageDeleteCommand implements Command { private final int page; private final RichText content; + private final int navigateDirection; - public PageDeleteCommand(int page, RichText content) { + public PageDeleteCommand(int page, RichText content, int navigateDirection) { this.page = page; this.content = content; + this.navigateDirection = navigateDirection; } @Override public void execute(HistoryListener listener) { - listener.deletePage(page); + listener.deletePage(page, navigateDirection); } @Override diff --git a/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java b/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java index 66b116d..7622743 100644 --- a/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java +++ b/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java @@ -18,6 +18,6 @@ public void execute(HistoryListener listener) { @Override public void rollback(HistoryListener listener) { - listener.deletePage(page); + listener.deletePage(page, -1); } } diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java index 48e51a8..d093ff0 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java @@ -7,11 +7,13 @@ import me.chrr.scribble.book.BookFile; import me.chrr.scribble.book.FileChooser; import me.chrr.scribble.book.RichText; +import me.chrr.scribble.book.TextOverflowHandler; import me.chrr.scribble.ScribbleConfig; import me.chrr.scribble.gui.TextArea; import me.chrr.scribble.gui.button.ColorSwatchWidget; import me.chrr.scribble.gui.button.IconButtonWidget; import me.chrr.scribble.gui.button.ModifierButtonWidget; +import me.chrr.scribble.gui.edit.OverflowHandler; import me.chrr.scribble.gui.edit.RichEditBox; import me.chrr.scribble.gui.edit.RichMultiLineTextField; import me.chrr.scribble.history.CommandManager; @@ -81,6 +83,8 @@ public class ScribbleBookEditScreen extends ScribbleBookScreen impleme private List colorSwatches = List.of(); + private @Nullable TextOverflowHandler overflowHandler; + public ScribbleBookEditScreen(Player player, ItemStack itemStack, InteractionHand hand, WritableBookContent book) { super(Component.translatable("book.edit.title")); @@ -170,7 +174,7 @@ protected void initPageButtons(int y) { addRenderableWidget(new IconButtonWidget(deleteText, () -> { PageDeleteCommand command = new PageDeleteCommand(this.currentPage + pageOffset, - this.pages.get(this.currentPage + pageOffset)); + this.pages.get(this.currentPage + pageOffset), 1); // Navigate right command.execute(this); commandManager.push(command); }, @@ -206,9 +210,24 @@ protected void setInitialFocus() { @Override protected TextArea createTextArea(int x, int y, int width, int height, int pageOffset) { + // Create the overflow handler lazily (needs font) + if (this.overflowHandler == null) { + this.overflowHandler = new TextOverflowHandler(this, this.font, (cmd) -> { + this.commandManager.push(cmd); + this.dirty = true; + this.invalidateActionButtons(); + }); + } + + // Create an overflow handler for this specific edit box + OverflowHandler editBoxOverflowHandler = Scribble.config().overflowWhenTyping.get() + ? createOverflowHandlerForPage(pageOffset) + : null; + RichEditBox editBox = (RichEditBox) new RichEditBox.Builder() .onHistoryPush((command) -> this.pushCommand(pageOffset, command)) .onInvalidateFormat(this::invalidateFormattingButtons) + .onOverflow(editBoxOverflowHandler) .setShowDecorations(false) .setTextColor(0xff000000).setCursorColor(0xff000000) .setShowBackground(false).setTextShadow(false) @@ -229,6 +248,39 @@ protected TextArea createTextArea(int x, int y, int width, int height, return editBox; } + private OverflowHandler createOverflowHandlerForPage(int pageOffset) { + return new OverflowHandler() { + @Override + public boolean handleOverflow(RichText currentText, int cursor, RichText insert, + @Nullable ChatFormatting color, Set modifiers) { + if (overflowHandler == null || getTotalPages() >= 100) return false; + int page = currentPage + pageOffset; + return overflowHandler.insertWithOverflow(page, currentText, cursor, insert, color, modifiers); + } + + @Override + public boolean handleEnterAtEnd() { + if (getTotalPages() >= 100) return false; + int page = currentPage + pageOffset; + PageInsertCommand command = new PageInsertCommand(page + 1); + command.execute(ScribbleBookEditScreen.this); + commandManager.push(command); + return true; + } + + @Override + public boolean handleBackspaceOnEmpty() { + if (getTotalPages() <= 1) return false; + int page = currentPage + pageOffset; + if (page <= 0) return false; + PageDeleteCommand command = new PageDeleteCommand(page, pages.get(page), -1); // Navigate left + command.execute(ScribbleBookEditScreen.this); + commandManager.push(command); + return true; + } + }; + } + private void updateFocusedEditBox() { if (this.getFocused() instanceof RichEditBox focusedEditBox && this.lastFocusedEditBox != focusedEditBox) { this.lastFocusedEditBox = focusedEditBox; @@ -415,7 +467,7 @@ protected RichText getPage(int page) { } @Override - protected int getTotalPages() { + public int getTotalPages() { return this.pages.size(); } @@ -506,12 +558,27 @@ public void insertPageAt(int page, @Nullable RichText content) { } @Override - public void deletePage(int page) { + public void deletePage(int page, int navigateDirection) { this.pages.remove(page); this.dirty = true; + int target = navigateDirection < 0 ? page - 1 : Math.min(page, this.pages.size() - 1); + this.showPage(Math.max(0, target), false); + this.updateCurrentPages(); + } - if (page >= this.pages.size() - 1) - this.showPage(page - 1, false); + @Override + public RichText getPageContent(int page) { + return this.pages.get(page); + } + + @Override + public void setPageContent(int page, RichText content) { + this.pages.set(page, content); + this.dirty = true; + } + + @Override + public void refreshPages() { this.updateCurrentPages(); } //endregion diff --git a/src/main/resources/assets/scribble/lang/en_us.json b/src/main/resources/assets/scribble/lang/en_us.json index 98dd9f8..3f6f9bb 100644 --- a/src/main/resources/assets/scribble/lang/en_us.json +++ b/src/main/resources/assets/scribble/lang/en_us.json @@ -11,6 +11,11 @@ "config.scribble.category.behaviour": "Behaviour", "config.scribble.option.copy_formatting_codes": "Copy formatting codes", "config.scribble.option.edit_history_size": "Edit history limit", + "config.scribble.option.overflow_when_typing": "Overflow when typing", + "config.scribble.option.paste_behavior": "Paste behavior", + "config.scribble.value.paste_behavior.deny": "Deny when full", + "config.scribble.value.paste_behavior.fit_page": "Fit to page", + "config.scribble.value.paste_behavior.overflow": "Overflow to new pages", "config.scribble.category.miscellaneous": "Miscellaneous", "config.scribble.option.open_vanilla_book_screen_on_shift": "Open vanilla GUIs when holding SHIFT", "text.tapestry.config.reset": "Reset to default value", From 048aed30c3aff7e79f4cc1f3e77bc8aa6a6aae5f Mon Sep 17 00:00:00 2001 From: J4PC Date: Sun, 15 Feb 2026 19:03:09 +0100 Subject: [PATCH 2/9] Hide delete button and filter line break symbols --- src/main/java/me/chrr/scribble/book/RichText.java | 13 +++++++++++++ .../java/me/chrr/scribble/gui/edit/RichEditBox.java | 2 +- .../scribble/screen/ScribbleBookEditScreen.java | 7 +++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/main/java/me/chrr/scribble/book/RichText.java b/src/main/java/me/chrr/scribble/book/RichText.java index 7990207..a2e8666 100644 --- a/src/main/java/me/chrr/scribble/book/RichText.java +++ b/src/main/java/me/chrr/scribble/book/RichText.java @@ -508,6 +508,19 @@ public String getAsFormattedString() { return out.toString(); } + /** + * Remove carriage return characters (\r) from the text. This is needed because + * Windows line endings (CRLF) contain \r characters that Minecraft books display + * as visible symbols instead of treating them as whitespace. + * + * @return a new RichText with all carriage returns removed. + */ + public RichText filterCarriageReturns() { + String plain = this.getPlainText(); + if (!plain.contains("\r")) return this; + return RichText.fromFormattedString(this.getAsFormattedString().replace("\r", "")); + } + /** * Get the rich text as a vanilla {@link MutableComponent}. Note that this text content is valid for * this client only! diff --git a/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java b/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java index 5fcf936..eea2b16 100644 --- a/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java +++ b/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java @@ -282,7 +282,7 @@ public boolean keyPressed(KeyEvent event) { // Handle Paste with different behaviors based on config if (event.isPaste()) { - String clipboardText = Minecraft.getInstance().keyboardHandler.getClipboard(); + String clipboardText = Minecraft.getInstance().keyboardHandler.getClipboard().replace("\r", ""); boolean keepFormatting = Scribble.config().copyFormattingCodes.get() ^ event.hasShiftDown(); if (!keepFormatting) clipboardText = ChatFormatting.stripFormatting(clipboardText); diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java index d093ff0..e0bb561 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java @@ -74,6 +74,7 @@ public class ScribbleBookEditScreen extends ScribbleBookScreen impleme private @Nullable IconButtonWidget redoButton; private final List insertPageButtons = new ArrayList<>(); + private final List deletePageButtons = new ArrayList<>(); private @Nullable ModifierButtonWidget boldButton; private @Nullable ModifierButtonWidget italicButton; @@ -149,6 +150,7 @@ private void invalidateActionButtons() { @Override protected void initPageButtons(int y) { this.insertPageButtons.clear(); + this.deletePageButtons.clear(); for (int i = 0; i < this.pagesToShow; i++) { int xOffset = this.pagesToShow == 1 @@ -171,14 +173,14 @@ protected void initPageButtons(int y) { commandManager.push(command); }, getBackgroundX() + 78 + xOffset + i * 126, y + 2, 12, 90, 12, 12))); - addRenderableWidget(new IconButtonWidget(deleteText, + this.deletePageButtons.add(addRenderableWidget(new IconButtonWidget(deleteText, () -> { PageDeleteCommand command = new PageDeleteCommand(this.currentPage + pageOffset, this.pages.get(this.currentPage + pageOffset), 1); // Navigate right command.execute(this); commandManager.push(command); }, - getBackgroundX() + 94 + xOffset + i * 126, y + 2, 0, 90, 12, 12)); + getBackgroundX() + 94 + xOffset + i * 126, y + 2, 0, 90, 12, 12))); } } @@ -186,6 +188,7 @@ protected void initPageButtons(int y) { public void updateCurrentPages() { super.updateCurrentPages(); this.insertPageButtons.forEach((button) -> button.visible = this.getTotalPages() < 100); + this.deletePageButtons.forEach((button) -> button.visible = this.getTotalPages() > 1); } @Override From a600d58201ba14058318da17312fc5e4a1e8e276 Mon Sep 17 00:00:00 2001 From: J4PC Date: Sun, 15 Feb 2026 19:15:41 +0100 Subject: [PATCH 3/9] German Translation --- .../resources/assets/scribble/lang/de_de.json | 24 +++++++++++++++---- .../resources/assets/scribble/lang/en_us.json | 6 ++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/resources/assets/scribble/lang/de_de.json b/src/main/resources/assets/scribble/lang/de_de.json index 011e6d7..2703d37 100644 --- a/src/main/resources/assets/scribble/lang/de_de.json +++ b/src/main/resources/assets/scribble/lang/de_de.json @@ -1,19 +1,33 @@ { "config.scribble.title": "Scribble-Einstellungen", - "config.scribble.option.copy_formatting_codes": "Formatierungscodes kopieren", - "config.scribble.description.copy_formatting_codes": "Diese Option kann vorübergehend deaktiviert werden, indem\ndie Umschalttaste beim Kopieren oder Einfügen von Text gedrückt wird.", + "config.scribble.category.appearance": "Darstellung", + "config.scribble.option.double_page_viewing": "Zwei Seiten anzeigen", "config.scribble.option.center_book_gui": "Buch-GUI vertikal zentrieren", + "config.scribble.option.show_formatting_buttons": "Formatierungsknöpfe anzeigen", "config.scribble.option.show_action_buttons": "Aktionsknöpfe anzeigen", - "config.scribble.option.show_action_buttons.always": "Immer", - "config.scribble.option.show_action_buttons.when_editing": "Beim bearbeiten", - "config.scribble.option.show_action_buttons.never": "Nie", + "config.scribble.value.show_action_buttons.always": "Immer", + "config.scribble.value.show_action_buttons.when_editing": "Nur beim Bearbeiten", + "config.scribble.value.show_action_buttons.never": "Nie", + "config.scribble.category.behaviour": "Funktion", + "config.scribble.option.copy_formatting_codes": "Formatierungscodes kopieren", + "config.scribble.description.copy_formatting_codes": "Diese Option kann vorübergehend deaktiviert werden, indem\ndie Umschalttaste beim Kopieren oder Einfügen von Text gedrückt wird.", "config.scribble.option.edit_history_size": "Länge der Rückgängig-/Wiederherstellungshistorie", + "config.scribble.option.overflow_when_typing": "Automatischer Seitenumbruch", + "config.scribble.option.paste_behavior": "Einfügeverhalten", + "config.scribble.value.paste_behavior.deny": "Ablehnen wenn es nicht passt", + "config.scribble.value.paste_behavior.fit_page": "Seite füllen", + "config.scribble.value.paste_behavior.overflow": "Auf neuen Seiten fortsetzen", + "config.scribble.category.miscellaneous": "Sonstiges", + "config.scribble.option.open_vanilla_book_screen_on_shift": "Vanilla-GUIs öffnen wenn SHIFT gehalten wird", + "text.tapestry.config.reset": "Auf Standardwert zurücksetzen", "text.scribble.action.delete_page": "Seite löschen", "text.scribble.action.insert_new_page": "Neue Seite einfügen", + "text.scribble.action.insert_new_page_here": "Neue Seite hier einfügen", "text.scribble.action.undo": "Rückgängig", "text.scribble.action.redo": "Wiederherstellen", "text.scribble.action.save_book_to_file": "Buch in Datei speichern...", "text.scribble.action.load_book_from_file": "Buch aus Datei laden...", + "text.scribble.action.settings": "Einstellungen öffnen...", "text.scribble.overwrite_warning.title": "Möchten Sie dieses Buch wirklich überschreiben?", "text.scribble.overwrite_warning.description": "Der aktuelle Inhalt geht unwiderruflich verloren! (Für immer!)", "text.scribble.quit_without_saving.title": "Möchten Sie wirklich ohne Speichern beenden?", diff --git a/src/main/resources/assets/scribble/lang/en_us.json b/src/main/resources/assets/scribble/lang/en_us.json index 3f6f9bb..9f71b89 100644 --- a/src/main/resources/assets/scribble/lang/en_us.json +++ b/src/main/resources/assets/scribble/lang/en_us.json @@ -13,9 +13,9 @@ "config.scribble.option.edit_history_size": "Edit history limit", "config.scribble.option.overflow_when_typing": "Overflow when typing", "config.scribble.option.paste_behavior": "Paste behavior", - "config.scribble.value.paste_behavior.deny": "Deny when full", - "config.scribble.value.paste_behavior.fit_page": "Fit to page", - "config.scribble.value.paste_behavior.overflow": "Overflow to new pages", + "config.scribble.value.paste_behavior.deny": "Deny when it doesn't fit", + "config.scribble.value.paste_behavior.fit_page": "Fill page", + "config.scribble.value.paste_behavior.overflow": "Allow overflow to new pages", "config.scribble.category.miscellaneous": "Miscellaneous", "config.scribble.option.open_vanilla_book_screen_on_shift": "Open vanilla GUIs when holding SHIFT", "text.tapestry.config.reset": "Reset to default value", From 6765249b105cb6ed61d069a23fe9488fd4f9dba6 Mon Sep 17 00:00:00 2001 From: J4PC Date: Sun, 10 May 2026 14:13:26 +0200 Subject: [PATCH 4/9] refactor: use pushCommand for consistency and simplify overflow handler --- .../screen/ScribbleBookEditScreen.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java index e0bb561..d94bbfb 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java @@ -170,7 +170,7 @@ protected void initPageButtons(int y) { () -> { PageInsertCommand command = new PageInsertCommand(this.currentPage + pageOffset); command.execute(this); - commandManager.push(command); + this.pushCommand(command); }, getBackgroundX() + 78 + xOffset + i * 126, y + 2, 12, 90, 12, 12))); this.deletePageButtons.add(addRenderableWidget(new IconButtonWidget(deleteText, @@ -178,7 +178,7 @@ protected void initPageButtons(int y) { PageDeleteCommand command = new PageDeleteCommand(this.currentPage + pageOffset, this.pages.get(this.currentPage + pageOffset), 1); // Navigate right command.execute(this); - commandManager.push(command); + this.pushCommand(command); }, getBackgroundX() + 94 + xOffset + i * 126, y + 2, 0, 90, 12, 12))); } @@ -215,11 +215,7 @@ protected void setInitialFocus() { protected TextArea createTextArea(int x, int y, int width, int height, int pageOffset) { // Create the overflow handler lazily (needs font) if (this.overflowHandler == null) { - this.overflowHandler = new TextOverflowHandler(this, this.font, (cmd) -> { - this.commandManager.push(cmd); - this.dirty = true; - this.invalidateActionButtons(); - }); + this.overflowHandler = new TextOverflowHandler(this, this.font, this::pushCommand); } // Create an overflow handler for this specific edit box @@ -267,7 +263,7 @@ public boolean handleEnterAtEnd() { int page = currentPage + pageOffset; PageInsertCommand command = new PageInsertCommand(page + 1); command.execute(ScribbleBookEditScreen.this); - commandManager.push(command); + pushCommand(command); return true; } @@ -278,7 +274,7 @@ public boolean handleBackspaceOnEmpty() { if (page <= 0) return false; PageDeleteCommand command = new PageDeleteCommand(page, pages.get(page), -1); // Navigate left command.execute(ScribbleBookEditScreen.this); - commandManager.push(command); + pushCommand(command); return true; } }; @@ -520,10 +516,15 @@ private boolean isEmpty() { //endregion //region History + public void pushCommand(Command command) { + this.pushCommand(0, command); + } + public void pushCommand(int pageOffset, Command command) { if (command instanceof EditCommand editCommand) { editCommand.page = this.currentPage + pageOffset; } + // OverflowCommand, PageInsertCommand, PageDeleteCommand have their state set at construction time this.commandManager.push(command); this.dirty = true; From 18f88532acce333855b1671967b09902be386439 Mon Sep 17 00:00:00 2001 From: J4PC Date: Sun, 10 May 2026 16:37:37 +0200 Subject: [PATCH 5/9] Revert "Merge upstream/main (26.1.2)" This reverts commit 4f7cf909f253a5ce2085cadef7de9190d940357b, reversing changes made to 6765249b105cb6ed61d069a23fe9488fd4f9dba6. --- .github/workflows/build.yml | 21 +- .github/workflows/release.yml | 59 ++--- .github/workflows/release_specific.yml | 49 +++++ .gitignore | 3 - CHANGELOG.md | 20 -- README.md | 10 +- build.gradle.kts | 90 ++++---- common/build.gradle.kts | 12 - .../java/me/chrr/scribble/ExceptionUtil.java | 43 ---- .../main/java/me/chrr/scribble/Scribble.java | 38 ---- fabric/build.gradle.kts | 135 +++++++++++- fabric/gradle.properties | 2 + .../chrr/scribble/fabric/ModMenuCompat.java | 2 - .../chrr/scribble/fabric/ScribbleFabric.java | 25 ++- fabric/src/main/resources/fabric.mod.json | 41 ++++ gradle.properties | 24 +- gradle/wrapper/gradle-wrapper.properties | 2 +- neoforge/build.gradle.kts | 131 +++++++++++ neoforge/gradle.properties | 2 + .../scribble/neoforge/ScribbleNeoForge.java | 25 ++- .../resources/META-INF/neoforge.mods.toml | 21 ++ neoforge/src/main/resources/pack.mcmeta | 6 + settings.gradle.kts | 13 +- .../java/me/chrr/scribble/KeyboardUtil.java | 0 src/main/java/me/chrr/scribble/Scribble.java | 62 ++++++ .../java/me/chrr/scribble/ScribbleConfig.java | 6 +- .../me/chrr/scribble/SetReturnScreen.java | 0 .../java/me/chrr/scribble/book/BookFile.java | 4 +- .../me/chrr/scribble/book/FileChooser.java | 4 +- .../java/me/chrr/scribble/book/RichText.java | 0 .../scribble/book/TextOverflowHandler.java | 0 .../me/chrr/scribble/gui/BookTextWidget.java | 8 +- .../chrr/scribble/gui/PageNumberWidget.java | 15 +- .../java/me/chrr/scribble/gui/TextArea.java | 0 .../gui/button/ColorSwatchWidget.java | 4 +- .../scribble/gui/button/IconButtonWidget.java | 4 +- .../gui/button/ModifierButtonWidget.java | 4 +- .../scribble/gui/edit/OverflowHandler.java | 0 .../chrr/scribble/gui/edit/RichEditBox.java | 16 +- .../gui/edit/RichMultiLineTextField.java | 12 +- .../chrr/scribble/history/CommandManager.java | 2 +- .../scribble/history/HistoryListener.java | 0 .../scribble/history/command/Command.java | 0 .../scribble/history/command/EditCommand.java | 0 .../history/command/OverflowCommand.java | 0 .../history/command/PageDeleteCommand.java | 0 .../history/command/PageInsertCommand.java | 0 .../scribble/mixin/BookSignScreenMixin.java | 16 +- .../mixin/ClientPacketListenerMixin.java | 2 +- .../scribble/mixin/KeyboardHandlerMixin.java | 0 .../chrr/scribble/mixin/LocalPlayerMixin.java | 2 +- .../chrr/scribble/mixin/MenuScreensMixin.java | 2 +- .../screen/ScribbleBookEditScreen.java | 35 +-- .../scribble/screen/ScribbleBookScreen.java | 20 +- .../screen/ScribbleBookViewScreen.java | 12 +- .../screen/ScribbleLecternScreen.java | 0 .../java/me/chrr/tapestry/config/Config.java | 16 ++ .../tapestry/config/ConfigEnvironment.java | 7 + .../me/chrr/tapestry/config/ConfigIo.java | 108 +++++++++ .../chrr/tapestry/config/NamingStrategy.java | 40 ++++ .../java/me/chrr/tapestry/config/Option.java | 20 ++ .../chrr/tapestry/config/ReflectedConfig.java | 206 ++++++++++++++++++ .../tapestry/config/annotation/Header.java | 15 ++ .../tapestry/config/annotation/Hidden.java | 14 ++ .../config/annotation/SerializeName.java | 22 ++ .../config/annotation/TranslationPrefix.java | 15 ++ .../config/annotation/UpgradeRewriter.java | 15 ++ .../chrr/tapestry/config/gui/IconButton.java | 52 +++++ .../chrr/tapestry/config/gui/OptionList.java | 142 ++++++++++++ .../chrr/tapestry/config/gui/OptionProxy.java | 27 +++ .../config/gui/TapestryConfigScreen.java | 140 ++++++++++++ .../gui/widget/BooleanOptionWidget.java | 40 ++++ .../config/gui/widget/EnumOptionWidget.java | 36 +++ .../config/gui/widget/OptionWidget.java | 105 +++++++++ .../gui/widget/ReadOnlyOptionWidget.java | 20 ++ .../config/gui/widget/SliderOptionWidget.java | 160 ++++++++++++++ .../tapestry/config/value/Constraint.java | 15 ++ .../tapestry/config/value/TrackedValue.java | 36 +++ .../me/chrr/tapestry/config/value/Value.java | 38 ++++ .../tapestry/config/value/VirtualValue.java | 41 ++++ .../main/resources/assets/scribble/banner.png | Bin .../main/resources/assets/scribble/icon.png | Bin .../resources/assets/scribble/lang/de_de.json | 0 .../resources/assets/scribble/lang/en_us.json | 5 +- .../resources/assets/scribble/lang/es_mx.json | 0 .../resources/assets/scribble/lang/ru_ru.json | 0 .../resources/assets/scribble/lang/tr_tr.json | 0 .../resources/assets/scribble/lang/zh_cn.json | 0 .../resources/assets/scribble/lang/zh_tw.json | 0 .../assets/scribble/textures/gui/book_2.png | Bin .../textures/gui/scribble_widgets.png | Bin .../tapestry-config/textures/gui/reset.png | Bin 0 -> 156 bytes .../main/resources/scribble.accesswidener | 2 +- .../main/resources/scribble.mixins.json | 0 stonecutter.gradle.kts | 19 +- versions/1.21.11/gradle.properties | 8 + versions/26.1/gradle.properties | 8 - 97 files changed, 1996 insertions(+), 380 deletions(-) create mode 100644 .github/workflows/release_specific.yml delete mode 100644 common/build.gradle.kts delete mode 100644 common/src/main/java/me/chrr/scribble/ExceptionUtil.java delete mode 100644 common/src/main/java/me/chrr/scribble/Scribble.java create mode 100644 fabric/gradle.properties create mode 100644 fabric/src/main/resources/fabric.mod.json create mode 100644 neoforge/gradle.properties create mode 100644 neoforge/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 neoforge/src/main/resources/pack.mcmeta rename {common/src => src}/main/java/me/chrr/scribble/KeyboardUtil.java (100%) create mode 100644 src/main/java/me/chrr/scribble/Scribble.java rename {common/src => src}/main/java/me/chrr/scribble/ScribbleConfig.java (95%) rename {common/src => src}/main/java/me/chrr/scribble/SetReturnScreen.java (100%) rename {common/src => src}/main/java/me/chrr/scribble/book/BookFile.java (94%) rename {common/src => src}/main/java/me/chrr/scribble/book/FileChooser.java (98%) rename {common/src => src}/main/java/me/chrr/scribble/book/RichText.java (100%) rename {common/src => src}/main/java/me/chrr/scribble/book/TextOverflowHandler.java (100%) rename {common/src => src}/main/java/me/chrr/scribble/gui/BookTextWidget.java (92%) rename {common/src => src}/main/java/me/chrr/scribble/gui/PageNumberWidget.java (90%) rename {common/src => src}/main/java/me/chrr/scribble/gui/TextArea.java (100%) rename {common/src => src}/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java (92%) rename {common/src => src}/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java (91%) rename {common/src => src}/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java (93%) rename {common/src => src}/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java (100%) rename {common/src => src}/main/java/me/chrr/scribble/gui/edit/RichEditBox.java (95%) rename {common/src => src}/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java (96%) rename {common/src => src}/main/java/me/chrr/scribble/history/CommandManager.java (94%) rename {common/src => src}/main/java/me/chrr/scribble/history/HistoryListener.java (100%) rename {common/src => src}/main/java/me/chrr/scribble/history/command/Command.java (100%) rename {common/src => src}/main/java/me/chrr/scribble/history/command/EditCommand.java (100%) rename {common/src => src}/main/java/me/chrr/scribble/history/command/OverflowCommand.java (100%) rename {common/src => src}/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java (100%) rename {common/src => src}/main/java/me/chrr/scribble/history/command/PageInsertCommand.java (100%) rename {common/src => src}/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java (76%) rename {common/src => src}/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java (93%) rename {common/src => src}/main/java/me/chrr/scribble/mixin/KeyboardHandlerMixin.java (100%) rename {common/src => src}/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java (94%) rename {common/src => src}/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java (93%) rename {common/src => src}/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java (93%) rename {common/src => src}/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java (90%) rename {common/src => src}/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java (84%) rename {common/src => src}/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java (100%) create mode 100644 src/main/java/me/chrr/tapestry/config/Config.java create mode 100644 src/main/java/me/chrr/tapestry/config/ConfigEnvironment.java create mode 100644 src/main/java/me/chrr/tapestry/config/ConfigIo.java create mode 100644 src/main/java/me/chrr/tapestry/config/NamingStrategy.java create mode 100644 src/main/java/me/chrr/tapestry/config/Option.java create mode 100644 src/main/java/me/chrr/tapestry/config/ReflectedConfig.java create mode 100644 src/main/java/me/chrr/tapestry/config/annotation/Header.java create mode 100644 src/main/java/me/chrr/tapestry/config/annotation/Hidden.java create mode 100644 src/main/java/me/chrr/tapestry/config/annotation/SerializeName.java create mode 100644 src/main/java/me/chrr/tapestry/config/annotation/TranslationPrefix.java create mode 100644 src/main/java/me/chrr/tapestry/config/annotation/UpgradeRewriter.java create mode 100644 src/main/java/me/chrr/tapestry/config/gui/IconButton.java create mode 100644 src/main/java/me/chrr/tapestry/config/gui/OptionList.java create mode 100644 src/main/java/me/chrr/tapestry/config/gui/OptionProxy.java create mode 100644 src/main/java/me/chrr/tapestry/config/gui/TapestryConfigScreen.java create mode 100644 src/main/java/me/chrr/tapestry/config/gui/widget/BooleanOptionWidget.java create mode 100644 src/main/java/me/chrr/tapestry/config/gui/widget/EnumOptionWidget.java create mode 100644 src/main/java/me/chrr/tapestry/config/gui/widget/OptionWidget.java create mode 100644 src/main/java/me/chrr/tapestry/config/gui/widget/ReadOnlyOptionWidget.java create mode 100644 src/main/java/me/chrr/tapestry/config/gui/widget/SliderOptionWidget.java create mode 100644 src/main/java/me/chrr/tapestry/config/value/Constraint.java create mode 100644 src/main/java/me/chrr/tapestry/config/value/TrackedValue.java create mode 100644 src/main/java/me/chrr/tapestry/config/value/Value.java create mode 100644 src/main/java/me/chrr/tapestry/config/value/VirtualValue.java rename {common/src => src}/main/resources/assets/scribble/banner.png (100%) rename {common/src => src}/main/resources/assets/scribble/icon.png (100%) rename {common/src => src}/main/resources/assets/scribble/lang/de_de.json (100%) rename {common/src => src}/main/resources/assets/scribble/lang/en_us.json (89%) rename {common/src => src}/main/resources/assets/scribble/lang/es_mx.json (100%) rename {common/src => src}/main/resources/assets/scribble/lang/ru_ru.json (100%) rename {common/src => src}/main/resources/assets/scribble/lang/tr_tr.json (100%) rename {common/src => src}/main/resources/assets/scribble/lang/zh_cn.json (100%) rename {common/src => src}/main/resources/assets/scribble/lang/zh_tw.json (100%) rename {common/src => src}/main/resources/assets/scribble/textures/gui/book_2.png (100%) rename {common/src => src}/main/resources/assets/scribble/textures/gui/scribble_widgets.png (100%) create mode 100644 src/main/resources/assets/tapestry-config/textures/gui/reset.png rename {common/src => src}/main/resources/scribble.accesswidener (99%) rename {common/src => src}/main/resources/scribble.mixins.json (100%) create mode 100644 versions/1.21.11/gradle.properties delete mode 100644 versions/26.1/gradle.properties diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9193b1..4251531 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,24 +14,13 @@ jobs: contents: read steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Setup JDK 25 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: '25' + java-version: '21' distribution: 'temurin' - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Build JARs - run: ./gradlew build - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: mod-snapshots - path: versions/*/build/libs/scribble-*.jar - retention-days: 7 \ No newline at end of file + - name: Run Build + run: ./gradlew buildAndCollect diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4ec7fb..e03edc1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,16 +1,7 @@ -name: Release +name: Publish on: workflow_dispatch: - inputs: - version: - description: 'Version number' - required: true - type: string - minecraftVersions: - description: 'Minecraft versions (optional, comma-separated)' - required: false - type: string jobs: release: @@ -20,49 +11,33 @@ jobs: contents: write steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Setup JDK 25 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: '25' + java-version: '21' distribution: 'temurin' - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + - name: Run Build + run: ./gradlew buildAndCollect - - name: Update version in gradle.properties - id: set-version + - name: Fetch changelog run: | - sed -i "s/^mod\.version=.*/mod\.version=${{ github.event.inputs.version }}/" gradle.properties + { + echo "CHANGELOG<> $GITHUB_ENV - - name: Publish to Modrinth and CurseForge - if: ${{ !github.event.inputs.versions }} - run: ./gradlew publishMods + - run: ./gradlew publishMods env: + CHANGELOG: ${{ env.CHANGELOG }} MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} - RELEASE: 'true' - - name: Publish specified versions to Modrinth and CurseForge - if: ${{ github.event.inputs.versions }} - run: ./gradlew publishModsSpecificVersions - env: - PUBLISH_VERSIONS: ${{ github.event.inputs.versions }} - MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} - CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} - RELEASE: 'true' - - - name: Commit and push version change - uses: stefanzweifel/git-auto-commit-action@v7 - with: - commit_message: 'chore: release ${{ github.event.inputs.version }}' - tag_name: v${{ github.event.inputs.version }} - - - name: Create GitHub release + - name: Create release uses: softprops/action-gh-release@v2 with: - tag_name: v${{ github.event.inputs.version }} - body: 'Read more on [Modrinth](https://modrinth.com/mod/scribble).' - files: versions/*/build/libs/scribble-*.jar \ No newline at end of file + body: ${{ env.CHANGELOG }} + files: build/libs/*.jar \ No newline at end of file diff --git a/.github/workflows/release_specific.yml b/.github/workflows/release_specific.yml new file mode 100644 index 0000000..2a83484 --- /dev/null +++ b/.github/workflows/release_specific.yml @@ -0,0 +1,49 @@ +name: Publish Specific + +on: + workflow_dispatch: + inputs: + version: + description: 'Minecraft version' + required: true + type: string + + +jobs: + release: + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Run Build + run: ./gradlew buildAndCollect + + - name: Fetch changelog + run: | + { + echo "CHANGELOG<> $GITHUB_ENV + + - run: ./gradlew :fabric:${{ inputs.version }}:publishMods :neoforge:${{ inputs.version }}:publishMods + env: + CHANGELOG: ${{ env.CHANGELOG }} + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + body: ${{ env.CHANGELOG }} + files: build/libs/*.jar \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4bf7829..83e6380 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,5 @@ build/ run/ logs/ -versions/ -!/versions - .idea/ .vscode/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 601958c..661a1c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,3 @@ -## 2.0.2 - -- Show error dialogs when saving and loading books fails. -- Don't reset current formatting when cursor updates, but doesn't move. -- Mark versions compatible with all past and future drop versions (e.g. `~26.1` becomes `>=26.1 <26.2`). - -## 2.0.1 - -- Update Tapestry, which fixes some config things. - -## 2.0.0 - -Scribble's been upgraded and is out of beta! Thanks to Mojang deobfuscating the game, you can now use the same JAR file -for both Fabric and NeoForge! There's lots of other changes too if you're upgrading from a 1.x version, so see the -previous changelog entries. - -- Update to 26.1, drop support for older versions. -- Use Tapestry Gradle to build the mod (and a merged JAR!) instead of Architectury. -- Always encode exported book files using UTF-8. - ## 2.0.0-beta2 - Move from YACL to a custom config system. diff --git a/README.md b/README.md index 7c829c7..e214ecf 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,16 @@ or [CurseForge](https://curseforge.com/minecraft/mc-mods/scribble). Scribble supports multiple Minecraft versions using [Stonecutter](https://stonecutter.kikugie.dev/). The easiest way to interact with this is by using an IDE such as IntelliJ. To switch between versions, -use the Gradle tasks under the `stonecutter` category. To run the game, use the `runActive{Fabric,Neoforge}` +use the Gradle tasks under the `stonecutter` category. To run the game, use the `runActive{Fabric,Forge,Neoforge}` tasks. -Make sure to switch back to the latest version to commit changes. +Make sure to switch back to `1.21.4` to commit changes. ### Release checklist -- Add a changelog entry to `CHANGELOG.md`. +- Update the version number. + - Change in `gradle.properties`. + - Add an entry in `CHANGELOG.md`. +- Commit and push a new tag. (example: `v1.2.3`) + - Tag name is the version number prefixed by `v`. - Manually trigger the Publish workflow on GitHub. diff --git a/build.gradle.kts b/build.gradle.kts index 0fe4d1b..1ba8df6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,66 +1,52 @@ -import me.chrr.tapestry.gradle.Environment - plugins { - id("me.chrr.tapestry.gradle") + id("dev.architectury.loom") + id("architectury-plugin") } -val root = stonecutter.current.project -val current = stonecutter.current.version - -fun sibling(name: String) = - stonecutter.node.sibling(name)!!.project +fun Project.hasProp(namespace: String, key: String) = hasProperty("$namespace.$key") +fun Project.prop(namespace: String, key: String) = property("$namespace.$key") as String -tapestry { - projects { - common = listOf(sibling("common")) - fabric = listOf(sibling("fabric")) - neoforge = listOf(sibling("neoforge")) - } +val current = stonecutter.current.version +val minecraft = prop("minecraft", "version") - versions { - minecraft = prop("minecraft.version") - fabricLoader = prop("fabric.loader.version") - neoforge = prop("neoforge.version") - } +group = prop("mod", "group") +version = "${prop("mod", "version")}+mc$current-common" +base.archivesName.set(prop("mod", "name")) - info { - id = "scribble" - version = prop("mod.version") +architectury.injectInjectables = false +architectury.common(stonecutter.tree.branches.mapNotNull { + if (stonecutter.current.project !in it) null + else if (!it.project.hasProp("loom", "platform")) null + else it.project.prop("loom", "platform") +}) - name = "Scribble" - description = "Expertly edit your books with rich formatting options, page utilities and more!" - authors = listOf("chrrrs") - license = "MIT" - environment = Environment.Client +repositories { + maven("https://maven.parchmentmc.org") +} - url = "https://github.com/chrrs/scribble" - sources = "https://github.com/chrrs/scribble" - issues = "https://github.com/chrrs/scribble/issues" +dependencies { + minecraft("com.mojang:minecraft:$minecraft") - icon = "assets/scribble/icon.png" - banner = "assets/scribble/banner.png" - } + @Suppress("UnstableApiUsage") + mappings(loom.layered { + officialMojangMappings() + parchment("org.parchmentmc.data:parchment-${prop("parchment", "version")}@zip") + }) - transform { - classTweaker = "scribble.accesswidener" - mixinConfigs.add("scribble.mixins.json") - } + modImplementation("net.fabricmc:fabric-loader:${prop("fabric", "loaderVersion")}") +} - depends { - minecraft.version(prop("minecraft.dependency").get()) - mod(fabric = "fabric-resource-loader-v1") - mod("tapestry_config") - mod("tapestry") - } +loom { + accessWidenerPath = rootProject.file("src/main/resources/scribble.accesswidener") +} - game { - runDir = rootProject.file("run") - username = "chrrz" - } +java { + targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_21 +} - publish { - readChangelogFrom(rootProject.file("CHANGELOG.md")) - modrinth = "yXAvIk0x" - curseforge = "1051344" +tasks { + jar { + from("LICENSE") } -} \ No newline at end of file +} diff --git a/common/build.gradle.kts b/common/build.gradle.kts deleted file mode 100644 index 2ed33df..0000000 --- a/common/build.gradle.kts +++ /dev/null @@ -1,12 +0,0 @@ -repositories { - maven("https://maven.chrr.me/releases") -} - -dependencies { - val root = stonecutter.node.sibling("")!!.project - fun tapestryModule(name: String) = - implementation("me.chrr.tapestry:$name:${root.property("tapestry.version")}") - - jij(tapestryModule("tapestry-base")!!) - jij(tapestryModule("tapestry-config")!!) -} \ No newline at end of file diff --git a/common/src/main/java/me/chrr/scribble/ExceptionUtil.java b/common/src/main/java/me/chrr/scribble/ExceptionUtil.java deleted file mode 100644 index faa63a6..0000000 --- a/common/src/main/java/me/chrr/scribble/ExceptionUtil.java +++ /dev/null @@ -1,43 +0,0 @@ -package me.chrr.scribble; - -import net.minecraft.ChatFormatting; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; -import org.jspecify.annotations.NullMarked; - -import java.util.Arrays; -import java.util.Optional; - -@NullMarked -public class ExceptionUtil { - private ExceptionUtil() { - } - - public static Component getExceptionSummary(Throwable throwable) { - MutableComponent out = Component.empty(); - - boolean first = true; - Throwable cause = throwable; - while (cause != null) { - if (first) { - first = false; - } else { - out.append("\n\n"); - out.append(Component.literal("caused by: ").withStyle(ChatFormatting.GRAY)); - } - - String header = cause.getClass().getName() + ": " + cause.getMessage(); - out.append(Component.literal(header).withStyle(ChatFormatting.GRAY)); - - Optional source = Arrays.stream(cause.getStackTrace()) - .map(StackTraceElement::toString) - .filter(s -> s.contains("me.chrr.")) - .findFirst(); - source.ifPresent(it -> out.append(Component.literal("\nat " + it).withStyle(ChatFormatting.DARK_GRAY))); - - cause = cause.getCause(); - } - - return out; - } -} diff --git a/common/src/main/java/me/chrr/scribble/Scribble.java b/common/src/main/java/me/chrr/scribble/Scribble.java deleted file mode 100644 index 2a111e8..0000000 --- a/common/src/main/java/me/chrr/scribble/Scribble.java +++ /dev/null @@ -1,38 +0,0 @@ -package me.chrr.scribble; - -import me.chrr.scribble.book.FileChooser; -import me.chrr.tapestry.base.Tapestry; -import me.chrr.tapestry.config.ReflectedConfig; -import me.chrr.tapestry.config.gui.TapestryConfigScreen; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.resources.Identifier; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.jspecify.annotations.NullMarked; - -import java.nio.file.Path; -import java.util.List; -import java.util.Objects; - -@NullMarked -public class Scribble { - public static final String MOD_ID = "scribble"; - public static final String VERSION = Objects.requireNonNull(Tapestry.PLATFORM_METHODS.getModVersion(MOD_ID)); - - public static final Logger LOGGER = LogManager.getLogger(); - - public static final Path BOOK_DIR = Tapestry.PLATFORM_METHODS.getGameDirectory().resolve("books"); - public static final ScribbleConfig CONFIG = ReflectedConfig.load(ScribbleConfig.class, "scribble.client.json", List.of("scribble.json")); - - public static void init() { - FileChooser.convertLegacyBooks(); - } - - public static Screen buildConfigScreen(Screen parent) { - return new TapestryConfigScreen(parent, CONFIG); - } - - public static Identifier id(String path) { - return Identifier.fromNamespaceAndPath(MOD_ID, path); - } -} diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index a9ef16c..37e32f4 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -1,14 +1,139 @@ +import me.modmuss50.mpp.ReleaseType + +plugins { + id("dev.architectury.loom") + id("architectury-plugin") + id("com.gradleup.shadow") + id("me.modmuss50.mod-publish-plugin") +} + +fun Project.hasProp(namespace: String, key: String) = hasProperty("$namespace.$key") +fun Project.prop(namespace: String, key: String) = property("$namespace.$key") as String + +val common = requireNotNull(stonecutter.node.sibling("")).project + +val current = stonecutter.current.version +val minecraft = common.prop("minecraft", "version") + +version = "${common.prop("mod", "version")}+mc$current-fabric" +base.archivesName.set(prop("mod", "name")) + +architectury { + platformSetupLoomIde() + fabric() +} + +val commonBundle: Configuration by configurations.creating +val shadowBundle: Configuration by configurations.creating + +configurations { + compileClasspath.get().extendsFrom(commonBundle) + runtimeClasspath.get().extendsFrom(commonBundle) + get("developmentFabric").extendsFrom(commonBundle) +} + repositories { maven("https://maven.terraformersmc.com/releases/") + maven("https://maven.isxander.dev/releases") } dependencies { - val root = stonecutter.node.sibling("")!!.project + minecraft("com.mojang:minecraft:$minecraft") + mappings(loom.officialMojangMappings()) + modImplementation("net.fabricmc:fabric-loader:${common.prop("fabric", "loaderVersion")}") + fun fabricApiModule(name: String) = - implementation(fabricApi.module(name, root.property("fabric.api.version") as String)) + modImplementation(fabricApi.module(name, common.prop("fabric", "apiVersion"))) + include(fabricApiModule("fabric-api-base")!!) + include(fabricApiModule("fabric-resource-loader-v1")!!) + + modCompileOnly("com.terraformersmc:modmenu:${common.prop("modmenu", "version")}") + + commonBundle(project(common.path, "namedElements")) { isTransitive = false } + shadowBundle(project(common.path, "transformProductionFabric")) { isTransitive = false } +} - jij(fabricApiModule("fabric-api-base")!!) - jij(fabricApiModule("fabric-resource-loader-v1")!!) +loom { + accessWidenerPath = common.project.loom.accessWidenerPath - compileOnly("com.terraformersmc:modmenu:18.0.0-alpha.5") + runConfigs.all { + isIdeConfigGenerated = false + runDir = "../../../run" + vmArgs("-Dmixin.debug.export=true") + } } + +java { + targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_21 +} + +tasks { + shadowJar { + configurations = listOf(shadowBundle) + archiveClassifier = "dev-shadow" + } + + remapJar { + injectAccessWidener = true + inputFile.set(shadowJar.get().archiveFile) + archiveClassifier = null + dependsOn(shadowJar) + } + + jar { + archiveClassifier = "dev" + } + + processResources { + // We construct our minecraft dependency string based on the versions provided in gradle.properties + val platformVersions = common.prop("platform", "versions").split(",") + val minecraftDependency = if (platformVersions.size == 1) + platformVersions.first() else ">=${platformVersions.first()} <=${platformVersions.last()}" + + filesMatching("fabric.mod.json") { + expand( + "version" to common.prop("mod", "version"), + "minecraft" to minecraftDependency, + ) + } + } + + register("buildAndCollect") { + group = "build" + from(remapJar.get().archiveFile) + into(rootProject.layout.buildDirectory.file("libs")) + dependsOn(build) + } + + publishMods.get().dependsOn("buildAndCollect") +} + +publishMods { + val versions = common.prop("platform", "versions").split(",") + val modVersion = common.prop("mod", "version") + changelog.set(providers.environmentVariable("CHANGELOG")) + + type = when { + modVersion.contains("alpha") -> ReleaseType.ALPHA + modVersion.contains("beta") -> ReleaseType.BETA + else -> ReleaseType.STABLE + } + + displayName.set("$modVersion - Fabric $minecraft") + version.set(project.version.toString()) + modLoaders.addAll(prop("platform", "loaders").split(",")) + file.set(tasks.remapJar.get().archiveFile) + + modrinth { + projectId.set(prop("modrinth", "id")) + accessToken.set(providers.environmentVariable("MODRINTH_TOKEN")) + minecraftVersions.addAll(versions) + } + + curseforge { + projectId.set(prop("curseforge", "id")) + accessToken.set(providers.environmentVariable("CURSEFORGE_TOKEN")) + minecraftVersions.addAll(versions) + } +} \ No newline at end of file diff --git a/fabric/gradle.properties b/fabric/gradle.properties new file mode 100644 index 0000000..c7150fa --- /dev/null +++ b/fabric/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=fabric +platform.loaders=fabric,quilt \ No newline at end of file diff --git a/fabric/src/main/java/me/chrr/scribble/fabric/ModMenuCompat.java b/fabric/src/main/java/me/chrr/scribble/fabric/ModMenuCompat.java index d59aef0..8908ae0 100644 --- a/fabric/src/main/java/me/chrr/scribble/fabric/ModMenuCompat.java +++ b/fabric/src/main/java/me/chrr/scribble/fabric/ModMenuCompat.java @@ -3,11 +3,9 @@ import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; import me.chrr.scribble.Scribble; -import me.chrr.tapestry.gradle.annotation.FabricEntrypoint; import org.jspecify.annotations.NullMarked; @NullMarked -@FabricEntrypoint("modmenu") public class ModMenuCompat implements ModMenuApi { @Override public ConfigScreenFactory getModConfigScreenFactory() { diff --git a/fabric/src/main/java/me/chrr/scribble/fabric/ScribbleFabric.java b/fabric/src/main/java/me/chrr/scribble/fabric/ScribbleFabric.java index 035b7cd..7bd84fb 100644 --- a/fabric/src/main/java/me/chrr/scribble/fabric/ScribbleFabric.java +++ b/fabric/src/main/java/me/chrr/scribble/fabric/ScribbleFabric.java @@ -1,15 +1,32 @@ package me.chrr.scribble.fabric; import me.chrr.scribble.Scribble; -import me.chrr.tapestry.gradle.annotation.FabricEntrypoint; import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.loader.api.FabricLoader; import org.jspecify.annotations.NullMarked; +import java.nio.file.Path; + @NullMarked -@FabricEntrypoint("client") -public class ScribbleFabric implements ClientModInitializer { +public class ScribbleFabric extends Scribble.Platform implements ClientModInitializer { @Override public void onInitializeClient() { - Scribble.init(); + Scribble.init(this); + } + + @Override + protected String getModVersion() { + return FabricLoader.getInstance().getModContainer(Scribble.MOD_ID) + .orElseThrow().getMetadata().getVersion().getFriendlyString(); + } + + @Override + protected Path getConfigDir() { + return FabricLoader.getInstance().getConfigDir(); + } + + @Override + protected Path getGameDir() { + return FabricLoader.getInstance().getGameDir(); } } \ No newline at end of file diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..36b9111 --- /dev/null +++ b/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "id": "scribble", + "version": "${version}", + "name": "Scribble", + "description": "Expertly edit your books with rich formatting options, page utilities and more!", + "authors": [ + "chrrrs" + ], + "contributors": [ + "WiskiWiski" + ], + "contact": { + "homepage": "https://modrinth.com/mod/scribble", + "sources": "https://github.com/chrrs/scribble", + "issues": "https://github.com/chrrs/scribble/issues" + }, + "license": "MIT", + "icon": "assets/scribble/icon.png", + "mixins": [ + "scribble.mixins.json" + ], + "environment": "client", + "entrypoints": { + "client": [ + "me.chrr.scribble.fabric.ScribbleFabric" + ], + "modmenu": [ + "me.chrr.scribble.fabric.ModMenuCompat" + ] + }, + "depends": { + "minecraft": "${minecraft}", + "fabricloader": ">=0.15", + "fabric-resource-loader-v1": "*" + }, + "breaks": { + "fixbookgui": "*", + "tapestry_config": "*" + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 2af4f57..532690f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,25 @@ org.gradle.jvmargs=-Xmx1G -fabric.loader.version=[versioned] -fabric.api.version=[versioned] +platform.versions=[versioned] +parchment.version=[versioned] + +# Mod details +mod.name=Scribble +mod.version=2.0.0-beta2 +mod.group=me.chrr + +# Fabric +# check these on https://modmuss50.me/fabric.html +fabric.loaderVersion=[versioned] +fabric.apiVersion=[versioned] + +# NeoForge +# check these on https://projects.neoforged.net/neoforged/neoforge neoforge.version=[versioned] -tapestry.version=[versioned] +# Dependencies +modmenu.version=11.0.2 -mod.version=2.0.2 \ No newline at end of file +# Distribution +modrinth.id=yXAvIk0x +curseforge.id=1051344 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7d8666c..7aa90e9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sun May 12 20:30:28 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts index e69de29..48ffd19 100644 --- a/neoforge/build.gradle.kts +++ b/neoforge/build.gradle.kts @@ -0,0 +1,131 @@ +import me.modmuss50.mpp.ReleaseType + +plugins { + id("dev.architectury.loom") + id("architectury-plugin") + id("com.gradleup.shadow") + id("me.modmuss50.mod-publish-plugin") +} + +fun Project.hasProp(namespace: String, key: String) = hasProperty("$namespace.$key") +fun Project.prop(namespace: String, key: String) = property("$namespace.$key") as String + +val common = requireNotNull(stonecutter.node.sibling("")).project + +val current = stonecutter.current.version +val minecraft = common.prop("minecraft", "version") + +version = "${common.prop("mod", "version")}+mc$current-neoforge" +base.archivesName.set(prop("mod", "name")) + +architectury { + platformSetupLoomIde() + neoForge() +} + +val commonBundle: Configuration by configurations.creating +val shadowBundle: Configuration by configurations.creating + +configurations { + compileClasspath.get().extendsFrom(commonBundle) + runtimeClasspath.get().extendsFrom(commonBundle) + get("developmentNeoForge").extendsFrom(commonBundle) +} + +repositories { + maven("https://maven.neoforged.net/releases/") +} + +dependencies { + minecraft("com.mojang:minecraft:$minecraft") + mappings(loom.officialMojangMappings()) + neoForge("net.neoforged:neoforge:${common.prop("neoforge", "version")}") + + commonBundle(project(common.path, "namedElements")) { isTransitive = false } + shadowBundle(project(common.path, "transformProductionNeoForge")) { isTransitive = false } +} + +loom { + accessWidenerPath = common.project.loom.accessWidenerPath + + runConfigs.all { + isIdeConfigGenerated = false + runDir = "../../../run" + vmArgs("-Dmixin.debug.export=true") + } +} + +java { + targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_21 +} + +tasks { + shadowJar { + configurations = listOf(shadowBundle) + archiveClassifier = "dev-shadow" + } + + remapJar { + atAccessWideners.add("scribble.accesswidener") + inputFile.set(shadowJar.get().archiveFile) + archiveClassifier = null + dependsOn(shadowJar) + } + + jar { + archiveClassifier = "dev" + } + + processResources { + // We construct our minecraft dependency string based on the versions provided in gradle.properties + val platformVersions = common.prop("platform", "versions").split(",") + val minecraftDependency = if (platformVersions.size == 1) + "[${platformVersions.first()}]" else "[${platformVersions.first()}, ${platformVersions.last()}]" + + filesMatching("META-INF/neoforge.mods.toml") { + expand( + "version" to common.prop("mod", "version"), + "minecraft" to minecraftDependency, + ) + } + } + + register("buildAndCollect") { + group = "build" + from(remapJar.get().archiveFile) + into(rootProject.layout.buildDirectory.file("libs")) + dependsOn(build) + } + + publishMods.get().dependsOn("buildAndCollect") +} + +publishMods { + val versions = common.prop("platform", "versions").split(",") + val modVersion = common.prop("mod", "version") + changelog.set(providers.environmentVariable("CHANGELOG")) + + type = when { + modVersion.contains("alpha") -> ReleaseType.ALPHA + modVersion.contains("beta") -> ReleaseType.BETA + else -> ReleaseType.STABLE + } + + displayName.set("$modVersion - NeoForge $minecraft") + version.set(project.version.toString()) + modLoaders.addAll(prop("platform", "loaders").split(",")) + file.set(tasks.remapJar.get().archiveFile) + + modrinth { + projectId.set(prop("modrinth", "id")) + accessToken.set(providers.environmentVariable("MODRINTH_TOKEN")) + minecraftVersions.addAll(versions) + } + + curseforge { + projectId.set(prop("curseforge", "id")) + accessToken.set(providers.environmentVariable("CURSEFORGE_TOKEN")) + minecraftVersions.addAll(versions) + } +} \ No newline at end of file diff --git a/neoforge/gradle.properties b/neoforge/gradle.properties new file mode 100644 index 0000000..8fb23d3 --- /dev/null +++ b/neoforge/gradle.properties @@ -0,0 +1,2 @@ +loom.platform=neoforge +platform.loaders=neoforge \ No newline at end of file diff --git a/neoforge/src/main/java/me/chrr/scribble/neoforge/ScribbleNeoForge.java b/neoforge/src/main/java/me/chrr/scribble/neoforge/ScribbleNeoForge.java index 405d167..66f6ac7 100644 --- a/neoforge/src/main/java/me/chrr/scribble/neoforge/ScribbleNeoForge.java +++ b/neoforge/src/main/java/me/chrr/scribble/neoforge/ScribbleNeoForge.java @@ -3,17 +3,36 @@ import me.chrr.scribble.Scribble; import net.neoforged.api.distmarker.Dist; import net.neoforged.fml.ModContainer; +import net.neoforged.fml.ModList; import net.neoforged.fml.common.Mod; +import net.neoforged.fml.loading.FMLPaths; import net.neoforged.neoforge.client.gui.IConfigScreenFactory; import org.jspecify.annotations.NullMarked; +import java.nio.file.Path; + @NullMarked @Mod(value = Scribble.MOD_ID, dist = Dist.CLIENT) -public class ScribbleNeoForge { +public class ScribbleNeoForge extends Scribble.Platform { public ScribbleNeoForge(ModContainer mod) { - Scribble.init(); + Scribble.init(this); mod.registerExtensionPoint(IConfigScreenFactory.class, - (_, parent) -> Scribble.buildConfigScreen(parent)); + (container, parent) -> Scribble.buildConfigScreen(parent)); + } + + @Override + protected String getModVersion() { + return ModList.get().getModFileById(Scribble.MOD_ID).versionString(); + } + + @Override + protected Path getConfigDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + protected Path getGameDir() { + return FMLPaths.GAMEDIR.get(); } } \ No newline at end of file diff --git a/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..33afa56 --- /dev/null +++ b/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,21 @@ +modLoader = "javafml" +loaderVersion = "[1,)" +issueTrackerURL = "https://github.com/chrrs/scribble/issues" +license = "MIT" + +[[mods]] +modId = "scribble" +version = "${version}" +displayName = "Scribble" +description = "Expertly edit your books with rich formatting options, page utilities and more!" +authors = "chrrrs" +credits = "WiskiWiski" +logoFile = "assets/scribble/banner.png" +logoBlur = false + +[[mixins]] +config = "scribble.mixins.json" + +[[dependencies.scribble]] +modId = "minecraft" +versionRange = "${minecraft}" \ No newline at end of file diff --git a/neoforge/src/main/resources/pack.mcmeta b/neoforge/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..995590b --- /dev/null +++ b/neoforge/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "Scribble", + "pack_format": 15 + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 23b4b27..b09220b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,14 +1,15 @@ pluginManagement { repositories { maven("https://maven.fabricmc.net/") + maven("https://maven.architectury.dev") + maven("https://maven.neoforged.net/releases/") maven("https://maven.kikugie.dev/snapshots") - maven("https://maven.chrr.me/releases") gradlePluginPortal() } } plugins { - id("dev.kikugie.stonecutter") version "0.9.2" + id("dev.kikugie.stonecutter") version "0.8.1" } stonecutter { @@ -16,11 +17,9 @@ stonecutter { centralScript = "build.gradle.kts" create(rootProject) { - versions("26.1") - - branch("common") - branch("fabric") - branch("neoforge") + versions("1.21.11") + branch("fabric") { versions("1.21.11") } + branch("neoforge") { versions("1.21.11") } } } diff --git a/common/src/main/java/me/chrr/scribble/KeyboardUtil.java b/src/main/java/me/chrr/scribble/KeyboardUtil.java similarity index 100% rename from common/src/main/java/me/chrr/scribble/KeyboardUtil.java rename to src/main/java/me/chrr/scribble/KeyboardUtil.java diff --git a/src/main/java/me/chrr/scribble/Scribble.java b/src/main/java/me/chrr/scribble/Scribble.java new file mode 100644 index 0000000..fd9ee2f --- /dev/null +++ b/src/main/java/me/chrr/scribble/Scribble.java @@ -0,0 +1,62 @@ +package me.chrr.scribble; + +import me.chrr.scribble.book.FileChooser; +import me.chrr.tapestry.config.ReflectedConfig; +import me.chrr.tapestry.config.gui.TapestryConfigScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.resources.Identifier; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; + +@NullMarked +public class Scribble { + public static final String MOD_ID = "scribble"; + public static final Logger LOGGER = LogManager.getLogger(); + + private static @Nullable Platform PLATFORM; + private static @Nullable ScribbleConfig CONFIG; + + public static void init(Platform platform) { + PLATFORM = platform; + + CONFIG = ReflectedConfig.load(() -> platform.CONFIG_DIR, ScribbleConfig.class, + "scribble.client.json", List.of("scribble.json")); + + FileChooser.convertLegacyBooks(); + } + + public static Screen buildConfigScreen(Screen parent) { + return new TapestryConfigScreen(parent, config()); + } + + public static ScribbleConfig config() { + return Objects.requireNonNull(CONFIG); + } + + public static Platform platform() { + return Objects.requireNonNull(PLATFORM); + } + + public static Identifier id(String path) { + return Identifier.fromNamespaceAndPath(MOD_ID, path); + } + + public abstract static class Platform { + public final Path CONFIG_DIR = getConfigDir(); + public final Path BOOK_DIR = getGameDir().resolve("books"); + + public final String VERSION = getModVersion(); + + protected abstract String getModVersion(); + + protected abstract Path getConfigDir(); + + protected abstract Path getGameDir(); + } +} diff --git a/common/src/main/java/me/chrr/scribble/ScribbleConfig.java b/src/main/java/me/chrr/scribble/ScribbleConfig.java similarity index 95% rename from common/src/main/java/me/chrr/scribble/ScribbleConfig.java rename to src/main/java/me/chrr/scribble/ScribbleConfig.java index 8d77414..7feb8f8 100644 --- a/common/src/main/java/me/chrr/scribble/ScribbleConfig.java +++ b/src/main/java/me/chrr/scribble/ScribbleConfig.java @@ -15,20 +15,20 @@ public class ScribbleConfig extends ReflectedConfig { @Hidden public Value pagesToShow = value(1); - @Category("appearance") + @Header("appearance") public Value doublePageViewing = map(pagesToShow, (n) -> n > 1, (b) -> b ? 2 : 1); public Value centerBookGui = value(true); public Value showFormattingButtons = value(true); public Value showActionButtons = value(ShowActionButtons.WHEN_EDITING); - @Category("behaviour") + @Header("behaviour") public Value copyFormattingCodes = value(true); public Value editHistorySize = value(32) .range(8, 128, 1); public Value overflowWhenTyping = value(false); public Value pasteBehavior = value(PasteBehavior.DENY); - @Category("miscellaneous") + @Header("miscellaneous") public Value openVanillaBookScreenOnShift = value(false); @UpgradeRewriter(currentVersion = 3) diff --git a/common/src/main/java/me/chrr/scribble/SetReturnScreen.java b/src/main/java/me/chrr/scribble/SetReturnScreen.java similarity index 100% rename from common/src/main/java/me/chrr/scribble/SetReturnScreen.java rename to src/main/java/me/chrr/scribble/SetReturnScreen.java diff --git a/common/src/main/java/me/chrr/scribble/book/BookFile.java b/src/main/java/me/chrr/scribble/book/BookFile.java similarity index 94% rename from common/src/main/java/me/chrr/scribble/book/BookFile.java rename to src/main/java/me/chrr/scribble/book/BookFile.java index c30b66f..5873a33 100644 --- a/common/src/main/java/me/chrr/scribble/book/BookFile.java +++ b/src/main/java/me/chrr/scribble/book/BookFile.java @@ -34,7 +34,7 @@ public static BookFile readFile(Path path) throws IOException { } else if (path.toString().endsWith(".json")) { return readJson(path); } else { - throw new IOException("Unrecognized file format."); + throw new IOException("unrecognized file format"); } } @@ -51,7 +51,7 @@ private static BookFile readNbt(Path path) throws IOException { CompoundTag root = NbtIo.read(path); if (root == null) { - throw new IOException("Could not read book NBT file."); + throw new IOException("could not read book nbt file"); } String author = root.getString("author").orElse(""); diff --git a/common/src/main/java/me/chrr/scribble/book/FileChooser.java b/src/main/java/me/chrr/scribble/book/FileChooser.java similarity index 98% rename from common/src/main/java/me/chrr/scribble/book/FileChooser.java rename to src/main/java/me/chrr/scribble/book/FileChooser.java index 8b9dd26..a628e7a 100644 --- a/common/src/main/java/me/chrr/scribble/book/FileChooser.java +++ b/src/main/java/me/chrr/scribble/book/FileChooser.java @@ -81,7 +81,7 @@ public static void chooseFile(boolean save, Consumer pathConsumer) { * @return the book directory. */ private static Path createAndGetBookDirectory() { - Path bookDir = Scribble.BOOK_DIR; + Path bookDir = Scribble.platform().BOOK_DIR; try { if (!Files.exists(bookDir)) { @@ -99,7 +99,7 @@ private static Path createAndGetBookDirectory() { * new JSON format. All old files are copied to the '_legacy' directory. */ public static void convertLegacyBooks() { - Path rootDir = Scribble.BOOK_DIR; + Path rootDir = Scribble.platform().BOOK_DIR; Path legacyDir = rootDir.resolve("_legacy"); if (!rootDir.toFile().isDirectory()) { diff --git a/common/src/main/java/me/chrr/scribble/book/RichText.java b/src/main/java/me/chrr/scribble/book/RichText.java similarity index 100% rename from common/src/main/java/me/chrr/scribble/book/RichText.java rename to src/main/java/me/chrr/scribble/book/RichText.java diff --git a/common/src/main/java/me/chrr/scribble/book/TextOverflowHandler.java b/src/main/java/me/chrr/scribble/book/TextOverflowHandler.java similarity index 100% rename from common/src/main/java/me/chrr/scribble/book/TextOverflowHandler.java rename to src/main/java/me/chrr/scribble/book/TextOverflowHandler.java diff --git a/common/src/main/java/me/chrr/scribble/gui/BookTextWidget.java b/src/main/java/me/chrr/scribble/gui/BookTextWidget.java similarity index 92% rename from common/src/main/java/me/chrr/scribble/gui/BookTextWidget.java rename to src/main/java/me/chrr/scribble/gui/BookTextWidget.java index 80d543f..34ef5dd 100644 --- a/common/src/main/java/me/chrr/scribble/gui/BookTextWidget.java +++ b/src/main/java/me/chrr/scribble/gui/BookTextWidget.java @@ -2,7 +2,7 @@ import net.minecraft.client.gui.ActiveTextCollector; import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.narration.NarratedElementType; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.navigation.ScreenRectangle; @@ -46,14 +46,14 @@ public BookTextWidget(int x, int y, int width, int height, Font font, Consumer onPageChange, int x, int y, Font font) this.afterCursor = Component.empty(); } - @Override - protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float deltaTicks) { if (this.isFocused()) { int x = this.getX() + this.width; x -= this.font.width(this.afterCursor); - graphics.text(this.font, this.afterCursor, x, this.getY(), CommonColors.BLACK, false); + graphics.drawString(this.font, this.afterCursor, x, this.getY(), CommonColors.BLACK, false); int cursorX = x; x -= this.font.width(this.input); - graphics.text(this.font, this.input, x, this.getY(), CommonColors.BLACK, false); + graphics.drawString(this.font, this.input, x, this.getY(), CommonColors.BLACK, false); x -= this.font.width(this.beforeCursor); - graphics.text(this.font, this.beforeCursor, x, this.getY(), CommonColors.BLACK, false); + graphics.drawString(this.font, this.beforeCursor, x, this.getY(), CommonColors.BLACK, false); if ((Util.getMillis() - this.lastSwitchFocusTime) / 300L % 2L == 0L) { graphics.fill(cursorX, this.getY() - 1, cursorX + 1, this.getY() + 1 + font.lineHeight, CommonColors.BLACK); @@ -78,7 +77,7 @@ protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouse Component text = this.isHovered() ? this.hoverText : this.text; int textWidth = font.width(text); - graphics.text(this.font, text, this.getX() + this.width - textWidth, this.getY(), color, false); + graphics.drawString(this.font, text, this.getX() + this.width - textWidth, this.getY(), color, false); } if (this.isHovered()) { @@ -93,6 +92,8 @@ protected void updateWidgetNarration(NarrationElementOutput narrationElementOutp @Override public boolean charTyped(CharacterEvent event) { + if (event.modifiers() != 0) + return false; if (event.codepoint() < '0' || event.codepoint() > '9') return false; if (this.input.length() >= 2 && !(this.input.equals("10") && event.codepoint() == '0')) diff --git a/common/src/main/java/me/chrr/scribble/gui/TextArea.java b/src/main/java/me/chrr/scribble/gui/TextArea.java similarity index 100% rename from common/src/main/java/me/chrr/scribble/gui/TextArea.java rename to src/main/java/me/chrr/scribble/gui/TextArea.java diff --git a/common/src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java b/src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java similarity index 92% rename from common/src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java rename to src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java index 98dab33..db36d0b 100644 --- a/common/src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java +++ b/src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java @@ -1,7 +1,7 @@ package me.chrr.scribble.gui.button; import net.minecraft.ChatFormatting; -import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractButton; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.narration.NarrationElementOutput; @@ -24,7 +24,7 @@ public ColorSwatchWidget(Component tooltip, ChatFormatting color, Runnable onCli } @Override - protected void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) { + protected void renderContents(GuiGraphics graphics, int mouseX, int mouseY, float delta) { if (isHoveredOrFocused() || toggled) { graphics.fill(getX(), getY(), getX() + width, getY() + height, isHoveredOrFocused() ? 0xffffffff : 0xffa0a0a0); } diff --git a/common/src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java b/src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java similarity index 91% rename from common/src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java rename to src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java index 07e35cc..8abb077 100644 --- a/common/src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java +++ b/src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java @@ -1,7 +1,7 @@ package me.chrr.scribble.gui.button; import me.chrr.scribble.Scribble; -import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractButton; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.narration.NarrationElementOutput; @@ -30,7 +30,7 @@ public IconButtonWidget(Component tooltip, Runnable onPress, int x, int y, int u } @Override - protected void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) { + protected void renderContents(GuiGraphics graphics, int mouseX, int mouseY, float delta) { int v = this.v + (!this.active ? 2 : (this.isHoveredOrFocused() ? 1 : 0)) * this.height; graphics.blit(RenderPipelines.GUI_TEXTURED, WIDGETS_TEXTURE, getX(), getY(), this.u, v, this.width, this.height, 128, 128); } diff --git a/common/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java b/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java similarity index 93% rename from common/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java rename to src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java index 330272b..4287757 100644 --- a/common/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java +++ b/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java @@ -1,7 +1,7 @@ package me.chrr.scribble.gui.button; import me.chrr.scribble.Scribble; -import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractButton; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.narration.NarrationElementOutput; @@ -37,7 +37,7 @@ public ModifierButtonWidget(Component tooltip, Consumer onToggle, int x } @Override - protected void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) { + protected void renderContents(GuiGraphics graphics, int mouseX, int mouseY, float delta) { // If the button is hovered or focused, we want it to be in front, so we slightly increase the height. int offset = this.isHoveredOrFocused() ? 1 : 0; diff --git a/common/src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java b/src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java similarity index 100% rename from common/src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java rename to src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java diff --git a/common/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java b/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java similarity index 95% rename from common/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java rename to src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java index ade3e9f..eea2b16 100644 --- a/common/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java +++ b/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java @@ -11,7 +11,7 @@ import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.MultiLineEditBox; import net.minecraft.client.gui.components.MultilineTextField; import net.minecraft.client.gui.narration.NarratedElementType; @@ -109,12 +109,12 @@ private int getCursorColor() { } @Override - protected void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float deltaTicks) { + protected void renderContents(GuiGraphics graphics, int mouseX, int mouseY, float deltaTicks) { RichText text = getRichTextField().getRichText(); // Draw the placeholder text if there's no content. if (text.isEmpty() && !this.isFocused()) { - graphics.textWithWordWrap(this.font, this.placeholder, this.getInnerLeft(), this.getInnerTop(), this.width - this.totalInnerPadding(), 0xcce0e0e0); + graphics.drawWordWrap(this.font, this.placeholder, this.getInnerLeft(), this.getInnerTop(), this.width - this.totalInnerPadding(), 0xcce0e0e0); return; } @@ -135,7 +135,7 @@ protected void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mo if (visible) { // AD-HOC: Draw the entire line in one call. Vanilla does this differently, I don't know why RichText lineText = text.subText(line.beginIndex(), line.endIndex()); - graphics.text(this.font, lineText.getAsMutableComponent(), x, y, this.textColor, this.textShadow); + graphics.drawString(this.font, lineText.getAsMutableComponent(), x, y, this.textColor, this.textShadow); RichText beforeCursor = text.subText(line.beginIndex(), cursor); lastX = x + this.font.width(beforeCursor); @@ -149,7 +149,7 @@ protected void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mo // Otherwise, just draw the line normally. if (visible) { RichText lineText = text.subText(line.beginIndex(), line.endIndex()); - graphics.text(this.font, lineText.getAsMutableComponent(), x, y, this.textColor, this.textShadow); + graphics.drawString(this.font, lineText.getAsMutableComponent(), x, y, this.textColor, this.textShadow); lastX = x + this.font.width(lineText) - 1; } @@ -162,7 +162,7 @@ protected void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mo // If we haven't drawn the cursor yet, it should be a '_' at the last draw position. if (blink && !cursorInText) { if (this.withinContentAreaTopBottom(lastY, lastY + this.font.lineHeight)) { - graphics.text(this.font, "_", lastX + 1, lastY, this.getCursorColor(), this.textShadow); + graphics.drawString(this.font, "_", lastX + 1, lastY, this.getCursorColor(), this.textShadow); } } @@ -283,7 +283,7 @@ public boolean keyPressed(KeyEvent event) { // Handle Paste with different behaviors based on config if (event.isPaste()) { String clipboardText = Minecraft.getInstance().keyboardHandler.getClipboard().replace("\r", ""); - boolean keepFormatting = Scribble.CONFIG.copyFormattingCodes.get() ^ event.hasShiftDown(); + boolean keepFormatting = Scribble.config().copyFormattingCodes.get() ^ event.hasShiftDown(); if (!keepFormatting) clipboardText = ChatFormatting.stripFormatting(clipboardText); RichText insert = ChatFormatting.stripFormatting(clipboardText).equals(clipboardText) @@ -297,7 +297,7 @@ public boolean keyPressed(KeyEvent event) { .splitLines(result, tf.width, net.minecraft.network.chat.Style.EMPTY).size() > tf.lineLimit; if (wouldOverflow) { - ScribbleConfig.PasteBehavior behavior = Scribble.CONFIG.pasteBehavior.get(); + ScribbleConfig.PasteBehavior behavior = Scribble.config().pasteBehavior.get(); if (behavior == ScribbleConfig.PasteBehavior.FIT_PAGE) { int lo = 0, hi = insert.getLength(), best = 0; while (lo <= hi) { diff --git a/common/src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java b/src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java similarity index 96% rename from common/src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java rename to src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java index 9a15a29..006dd72 100644 --- a/common/src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java +++ b/src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java @@ -108,9 +108,6 @@ public void setValueWithoutUpdating(RichText richText) { } public void resetCursor(boolean update) { - if (this.cursor == this.value.length() && this.selectCursor == this.cursor) - return; - this.cursor = this.value.length(); this.selectCursor = this.cursor; @@ -190,19 +187,14 @@ public void seekCursorToPoint(double x, double y) { @Override public void seekCursor(Whence whence, int amount) { - int cursorBefore = this.cursor; - int selectBefore = this.selectCursor; - super.seekCursor(whence, amount); - - if (cursorBefore != this.cursor || selectBefore != this.selectCursor) - this.sendUpdateFormat(); + this.sendUpdateFormat(); } @Override public boolean keyPressed(KeyEvent event) { // Override copy/cut/paste to remove formatting codes if the config option is set or SHIFT is held down. - boolean keepFormatting = Scribble.CONFIG.copyFormattingCodes.get() ^ event.hasShiftDown(); + boolean keepFormatting = Scribble.config().copyFormattingCodes.get() ^ event.hasShiftDown(); boolean ctrlNoAlt = event.hasControlDown() && !event.hasAltDown(); if (ctrlNoAlt && (KeyboardUtil.isKey(event.key(), "C") || KeyboardUtil.isKey(event.key(), "X"))) { String text = this.getSelectedText(); diff --git a/common/src/main/java/me/chrr/scribble/history/CommandManager.java b/src/main/java/me/chrr/scribble/history/CommandManager.java similarity index 94% rename from common/src/main/java/me/chrr/scribble/history/CommandManager.java rename to src/main/java/me/chrr/scribble/history/CommandManager.java index 84869a4..d5fca7c 100644 --- a/common/src/main/java/me/chrr/scribble/history/CommandManager.java +++ b/src/main/java/me/chrr/scribble/history/CommandManager.java @@ -26,7 +26,7 @@ public void push(Command command) { this.commands.add(command); this.index += 1; - if (this.commands.size() > Scribble.CONFIG.editHistorySize.get()) { + if (this.commands.size() > Scribble.config().editHistorySize.get()) { this.commands.removeFirst(); this.index -= 1; } diff --git a/common/src/main/java/me/chrr/scribble/history/HistoryListener.java b/src/main/java/me/chrr/scribble/history/HistoryListener.java similarity index 100% rename from common/src/main/java/me/chrr/scribble/history/HistoryListener.java rename to src/main/java/me/chrr/scribble/history/HistoryListener.java diff --git a/common/src/main/java/me/chrr/scribble/history/command/Command.java b/src/main/java/me/chrr/scribble/history/command/Command.java similarity index 100% rename from common/src/main/java/me/chrr/scribble/history/command/Command.java rename to src/main/java/me/chrr/scribble/history/command/Command.java diff --git a/common/src/main/java/me/chrr/scribble/history/command/EditCommand.java b/src/main/java/me/chrr/scribble/history/command/EditCommand.java similarity index 100% rename from common/src/main/java/me/chrr/scribble/history/command/EditCommand.java rename to src/main/java/me/chrr/scribble/history/command/EditCommand.java diff --git a/common/src/main/java/me/chrr/scribble/history/command/OverflowCommand.java b/src/main/java/me/chrr/scribble/history/command/OverflowCommand.java similarity index 100% rename from common/src/main/java/me/chrr/scribble/history/command/OverflowCommand.java rename to src/main/java/me/chrr/scribble/history/command/OverflowCommand.java diff --git a/common/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java b/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java similarity index 100% rename from common/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java rename to src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java diff --git a/common/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java b/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java similarity index 100% rename from common/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java rename to src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java diff --git a/common/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java b/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java similarity index 76% rename from common/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java rename to src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java index 25f0353..fc651b3 100644 --- a/common/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java +++ b/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java @@ -6,7 +6,7 @@ import me.chrr.scribble.SetReturnScreen; import me.chrr.scribble.screen.ScribbleBookScreen; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.Renderable; import net.minecraft.client.gui.components.events.GuiEventListener; @@ -39,7 +39,7 @@ private BookSignScreenMixin(Component title) { this.scribble$returnScreen = screen; } - @WrapOperation(method = "lambda$init$2", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;setScreen(Lnet/minecraft/client/gui/screens/Screen;)V")) + @WrapOperation(method = "method_71541", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;setScreen(Lnet/minecraft/client/gui/screens/Screen;)V")) public void redirectReturnScreen(Minecraft instance, Screen screen, Operation original) { original.call(instance, this.scribble$returnScreen != null ? this.scribble$returnScreen : screen); } @@ -47,7 +47,7 @@ public void redirectReturnScreen(Minecraft instance, Screen screen, Operation T shiftButton // When rendering, we translate the matrices of the draw context to draw the text further down if needed. // Note that this happens after the parent screen render, so only the text in the book is shifted. - @Inject(method = "extractRenderState", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/Screen;extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", shift = At.Shift.AFTER)) - public void translateRender(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta, CallbackInfo ci) { + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/Screen;render(Lnet/minecraft/client/gui/GuiGraphics;IIF)V", shift = At.Shift.AFTER)) + public void translateRender(GuiGraphics graphics, int mouseX, int mouseY, float delta, CallbackInfo ci) { graphics.pose().pushMatrix(); graphics.pose().translate(0f, scribble$getYOffset()); } // At the end of rendering, we need to pop those matrices we pushed. - @Inject(method = "extractRenderState", at = @At(value = "RETURN")) - public void popRender(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta, CallbackInfo ci) { + @Inject(method = "render", at = @At(value = "RETURN")) + public void popRender(GuiGraphics graphics, int mouseX, int mouseY, float delta, CallbackInfo ci) { graphics.pose().popMatrix(); } @Unique private int scribble$getYOffset() { - if (Scribble.CONFIG.centerBookGui.get()) { + if (Scribble.config().centerBookGui.get()) { // See ScribbleBookScreen#getBackgroundY(). return this.height / 3 - ScribbleBookScreen.getMenuHeight() / 3; } else { diff --git a/common/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java b/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java similarity index 93% rename from common/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java rename to src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java index de8b6a8..0819801 100644 --- a/common/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java +++ b/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java @@ -18,7 +18,7 @@ public abstract class ClientPacketListenerMixin { @WrapOperation(method = "handleOpenBook", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;setScreen(Lnet/minecraft/client/gui/screens/Screen;)V")) public void overrideBookViewScreen(Minecraft instance, Screen screen, Operation original, @Local BookViewScreen.BookAccess book) { - if (instance.hasShiftDown() && Scribble.CONFIG.openVanillaBookScreenOnShift.get()) { + if (instance.hasShiftDown() && Scribble.config().openVanillaBookScreenOnShift.get()) { original.call(instance, screen); } else { // FIXME: ideally, I'd like to avoid even constructing the original BookViewScreen. diff --git a/common/src/main/java/me/chrr/scribble/mixin/KeyboardHandlerMixin.java b/src/main/java/me/chrr/scribble/mixin/KeyboardHandlerMixin.java similarity index 100% rename from common/src/main/java/me/chrr/scribble/mixin/KeyboardHandlerMixin.java rename to src/main/java/me/chrr/scribble/mixin/KeyboardHandlerMixin.java diff --git a/common/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java b/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java similarity index 94% rename from common/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java rename to src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java index d65b7a6..ee79256 100644 --- a/common/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java +++ b/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java @@ -20,7 +20,7 @@ public abstract class LocalPlayerMixin { @WrapOperation(method = "openItemGui", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;setScreen(Lnet/minecraft/client/gui/screens/Screen;)V")) public void overrideBookViewScreen(Minecraft instance, Screen screen, Operation original, @Local(argsOnly = true) ItemStack itemStack, @Local(argsOnly = true) InteractionHand hand, @Local WritableBookContent book) { - if (instance.hasShiftDown() && Scribble.CONFIG.openVanillaBookScreenOnShift.get()) { + if (instance.hasShiftDown() && Scribble.config().openVanillaBookScreenOnShift.get()) { original.call(instance, screen); } else { // FIXME: ideally, I'd like to avoid even constructing the original BookEditScreen. diff --git a/common/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java b/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java similarity index 93% rename from common/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java rename to src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java index f083fff..a4b4183 100644 --- a/common/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java +++ b/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java @@ -22,7 +22,7 @@ private static > void overrideLectern if (type == MenuType.LECTERN) { original.call(type, (MenuScreens.ScreenConstructor) (menu, inventory, title) -> { Minecraft minecraft = Minecraft.getInstance(); - if (!minecraft.hasShiftDown() || !Scribble.CONFIG.openVanillaBookScreenOnShift.get()) { + if (!minecraft.hasShiftDown() || !Scribble.config().openVanillaBookScreenOnShift.get()) { return (S) new ScribbleLecternScreen(menu); } else { return factory.create(menu, inventory, title); diff --git a/common/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java similarity index 93% rename from common/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java rename to src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java index 881de47..d94bbfb 100644 --- a/common/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java @@ -1,11 +1,14 @@ package me.chrr.scribble.screen; import it.unimi.dsi.fastutil.booleans.BooleanConsumer; -import me.chrr.scribble.*; +import me.chrr.scribble.KeyboardUtil; +import me.chrr.scribble.Scribble; +import me.chrr.scribble.SetReturnScreen; import me.chrr.scribble.book.BookFile; import me.chrr.scribble.book.FileChooser; import me.chrr.scribble.book.RichText; import me.chrr.scribble.book.TextOverflowHandler; +import me.chrr.scribble.ScribbleConfig; import me.chrr.scribble.gui.TextArea; import me.chrr.scribble.gui.button.ColorSwatchWidget; import me.chrr.scribble.gui.button.IconButtonWidget; @@ -24,7 +27,6 @@ import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.screens.AlertScreen; import net.minecraft.client.gui.screens.ConfirmScreen; import net.minecraft.client.gui.screens.inventory.BookSignScreen; import net.minecraft.client.input.KeyEvent; @@ -105,7 +107,7 @@ public ScribbleBookEditScreen(Player player, ItemStack itemStack, InteractionHan //region Widgets (Action, Menu & TextArea) @Override protected boolean shouldShowActionButtons() { - return Scribble.CONFIG.showActionButtons.get() != ScribbleConfig.ShowActionButtons.NEVER; + return Scribble.config().showActionButtons.get() != ScribbleConfig.ShowActionButtons.NEVER; } @Override @@ -153,8 +155,8 @@ protected void initPageButtons(int y) { for (int i = 0; i < this.pagesToShow; i++) { int xOffset = this.pagesToShow == 1 ? 0 : i == 0 - ? 32 : i == this.pagesToShow - 1 - ? -32 : 0; + ? 32 : i == this.pagesToShow - 1 + ? -32 : 0; // When we only show a single page, it's clearer to show something like // 'insert new page _before_ current' instead of just 'here'. @@ -217,7 +219,7 @@ protected TextArea createTextArea(int x, int y, int width, int height, } // Create an overflow handler for this specific edit box - OverflowHandler editBoxOverflowHandler = Scribble.CONFIG.overflowWhenTyping.get() + OverflowHandler editBoxOverflowHandler = Scribble.config().overflowWhenTyping.get() ? createOverflowHandlerForPage(pageOffset) : null; @@ -326,12 +328,6 @@ private void saveToFile(Path path) { bookFile.writeJson(path); } catch (Exception e) { Scribble.LOGGER.error("could not save book to file", e); - this.minecraft.setScreen(new AlertScreen( - () -> this.minecraft.setScreen(this), - Component.translatable("text.scribble.error.save_book.title"), - Component.translatable("text.scribble.error.summary_below") - .append("\n\n\n").append(ExceptionUtil.getExceptionSummary(e)) - )); } } @@ -340,11 +336,7 @@ private void loadFromFile(Path path) { BookFile bookFile = BookFile.readFile(path); this.pages.clear(); - - //noinspection ConstantValue: this isn - if (bookFile.pages() != null) - this.pages.addAll(bookFile.pages().stream().map(RichText::fromFormattedString).toList()); - + this.pages.addAll(bookFile.pages().stream().map(RichText::fromFormattedString).toList()); this.commandManager.clear(); this.dirty = true; @@ -352,13 +344,6 @@ private void loadFromFile(Path path) { this.updateCurrentPages(); } catch (Exception e) { Scribble.LOGGER.error("could not load book from file", e); - this.minecraft.setScreen(new AlertScreen( - () -> this.minecraft.setScreen(this), - Component.translatable("text.scribble.error.load_book.title"), - Component.translatable("text.scribble.error.load_book.subtitle") - .append("\n").append(Component.translatable("text.scribble.error.summary_below")) - .append("\n\n\n").append(ExceptionUtil.getExceptionSummary(e)) - )); } } @@ -399,7 +384,7 @@ public boolean keyPressed(KeyEvent keyEvent) { protected void init() { super.init(); - if (Scribble.CONFIG.showFormattingButtons.get()) { + if (Scribble.config().showFormattingButtons.get()) { int x = this.width / 2 + this.getBackgroundWidth() / 2 - 20; int y = this.getBackgroundY() + 12; diff --git a/common/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java similarity index 90% rename from common/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java rename to src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java index 2629fcc..bc6eca4 100644 --- a/common/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java @@ -4,7 +4,7 @@ import me.chrr.scribble.gui.PageNumberWidget; import me.chrr.scribble.gui.TextArea; import me.chrr.scribble.gui.button.IconButtonWidget; -import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.BookViewScreen; @@ -43,7 +43,7 @@ protected ScribbleBookScreen(Component title) { //region Widgets @Override protected void init() { - this.pagesToShow = Scribble.CONFIG.pagesToShow.get(); + this.pagesToShow = Scribble.config().pagesToShow.get(); if (this.pagesToShow == 1) { this.backgroundTexture = BookViewScreen.BOOK_LOCATION; } else { @@ -68,10 +68,10 @@ protected void init() { initMenuControls(getMenuControlsY()); this.backButton = addRenderableWidget(new PageButton(x + 43, y + 157, false, - (_) -> this.goPageBackward(this.minecraft.hasShiftDown()), this.shouldPlayTurnSound())); + (button) -> this.goPageBackward(this.minecraft.hasShiftDown()), this.shouldPlayTurnSound())); initPageButtons(y + 157); this.forwardButton = addRenderableWidget(new PageButton(x + 126 * this.pagesToShow - 10, y + 157, true, - (_) -> this.goPageForward(this.minecraft.hasShiftDown()), this.shouldPlayTurnSound())); + (button) -> this.goPageForward(this.minecraft.hasShiftDown()), this.shouldPlayTurnSound())); if (shouldShowActionButtons()) { initActionButtons(x, this.getBackgroundY() + 12 + 4); @@ -82,7 +82,7 @@ protected void init() { } private void initSettingsButton(int x, int y) { - MutableComponent settingsText = Component.literal("Scribble " + Scribble.VERSION + "\n") + MutableComponent settingsText = Component.literal("Scribble " + Scribble.platform().VERSION + "\n") .setStyle(Style.EMPTY.withBold(true)) .append(Component.translatable("text.scribble.action.settings") .setStyle(Style.EMPTY.withBold(false))); @@ -126,7 +126,7 @@ public void jumpToPage(int page) { public void showPage(int page, boolean insertIfMissing) { // Insert pages so the requested page exists if needed. - // noinspection MathClampMigration: Math.clamp checks for min>max, which isn't necessarily true here. + // (note that min+max instead of clamp here is deliberate) int newPage = Math.max(Math.min(page, this.getTotalPages() - 1), 0); if (insertIfMissing) { while (newPage < page && this.canInsertPages()) { @@ -179,11 +179,11 @@ public boolean keyPressed(KeyEvent event) { //region Rendering and dimensions @Override - public void extractBackground(GuiGraphicsExtractor GuiGraphicsExtractor, int mouseX, int mouseY, float a) { - super.extractBackground(GuiGraphicsExtractor, mouseX, mouseY, a); + public void renderBackground(GuiGraphics guiGraphics, int i, int j, float f) { + super.renderBackground(guiGraphics, i, j, f); int textureSize = this.pagesToShow == 1 ? 256 : 512; - GuiGraphicsExtractor.blit(RenderPipelines.GUI_TEXTURED, backgroundTexture, this.getBackgroundX(), this.getBackgroundY(), + guiGraphics.blit(RenderPipelines.GUI_TEXTURED, backgroundTexture, this.getBackgroundX(), this.getBackgroundY(), 0.0F, 0.0F, 122 * this.pagesToShow + 70, 192, textureSize, textureSize); } @@ -203,7 +203,7 @@ public int getBackgroundX() { } public int getBackgroundY() { - if (Scribble.CONFIG.centerBookGui.get()) { + if (Scribble.config().centerBookGui.get()) { // Perfect centering actually doesn't look great, so we put it on a third. return 2 + this.height / 3 - getMenuHeight() / 3; } else { diff --git a/common/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java similarity index 84% rename from common/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java rename to src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java index 37a2551..84e9493 100644 --- a/common/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java @@ -1,16 +1,14 @@ package me.chrr.scribble.screen; -import me.chrr.scribble.ExceptionUtil; import me.chrr.scribble.Scribble; -import me.chrr.scribble.ScribbleConfig; import me.chrr.scribble.book.BookFile; import me.chrr.scribble.book.FileChooser; import me.chrr.scribble.book.RichText; +import me.chrr.scribble.ScribbleConfig; import me.chrr.scribble.gui.BookTextWidget; import me.chrr.scribble.gui.TextArea; import me.chrr.scribble.gui.button.IconButtonWidget; import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.screens.AlertScreen; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.BookViewScreen; import net.minecraft.network.chat.ClickEvent; @@ -32,7 +30,7 @@ public ScribbleBookViewScreen(BookViewScreen.BookAccess book) { @Override protected boolean shouldShowActionButtons() { - return Scribble.CONFIG.showActionButtons.get() == ScribbleConfig.ShowActionButtons.ALWAYS; + return Scribble.config().showActionButtons.get() == ScribbleConfig.ShowActionButtons.ALWAYS; } @Override @@ -77,12 +75,6 @@ private void saveBookToFile() { bookFile.writeJson(path); } catch (Exception e) { Scribble.LOGGER.error("could not save book to file", e); - this.minecraft.setScreen(new AlertScreen( - () -> this.minecraft.setScreen(this), - Component.translatable("text.scribble.error.save_book.title"), - Component.translatable("text.scribble.error.summary_below") - .append("\n\n\n").append(ExceptionUtil.getExceptionSummary(e)) - )); } }); } diff --git a/common/src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java similarity index 100% rename from common/src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java rename to src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java diff --git a/src/main/java/me/chrr/tapestry/config/Config.java b/src/main/java/me/chrr/tapestry/config/Config.java new file mode 100644 index 0000000..5a4b1c4 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/Config.java @@ -0,0 +1,16 @@ +package me.chrr.tapestry.config; + +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.util.Collection; + +public interface Config { + Collection> getOptions(); + + Component getTitle(); + + ConfigIo.@Nullable UpgradeRewriter getUpgradeRewriter(); + + void save(); +} diff --git a/src/main/java/me/chrr/tapestry/config/ConfigEnvironment.java b/src/main/java/me/chrr/tapestry/config/ConfigEnvironment.java new file mode 100644 index 0000000..6a8fcd1 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/ConfigEnvironment.java @@ -0,0 +1,7 @@ +package me.chrr.tapestry.config; + +import java.nio.file.Path; + +public interface ConfigEnvironment { + Path getConfigDir(); +} diff --git a/src/main/java/me/chrr/tapestry/config/ConfigIo.java b/src/main/java/me/chrr/tapestry/config/ConfigIo.java new file mode 100644 index 0000000..9e84013 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/ConfigIo.java @@ -0,0 +1,108 @@ +package me.chrr.tapestry.config; + +import com.google.gson.*; +import me.chrr.tapestry.config.value.Value; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jspecify.annotations.NullMarked; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +@NullMarked +public enum ConfigIo { + ; + + private static final Logger LOGGER = LogManager.getLogger("Tapestry/ConfigIo"); + + public static final Gson GSON = new GsonBuilder() + .setStrictness(Strictness.LENIENT) + .setFormattingStyle(FormattingStyle.PRETTY) + .create(); + + public static void loadFromPathOrSaveDefault(Config config, Path file, List aliases) { + if (Files.exists(file)) { + loadFromPath(config, file); + return; + } else { + for (Path alias : aliases) { + if (!Files.isRegularFile(alias)) + continue; + + loadFromPath(config, alias); + saveToPath(config, file); + + try { + Files.delete(alias); + } catch (IOException e) { + LOGGER.error("Failed to delete old config file at '{}'", alias, e); + } + + LOGGER.info("Migrated config from '{}' to '{}'", alias, file); + return; + } + } + + LOGGER.info("No config file found, saving default config to '{}'", file); + saveToPath(config, file); + } + + public static void loadFromPath(Config config, Path path) { + try { + JsonObject object = JsonParser.parseString(Files.readString(path)).getAsJsonObject(); + + UpgradeRewriter upgradeRewriter = config.getUpgradeRewriter(); + if (upgradeRewriter != null && object.has("version")) { + int version = object.get("version").getAsInt(); + upgradeRewriter.upgrade(version, object); + object.addProperty("version", upgradeRewriter.getLatestVersion()); + LOGGER.info("Upgraded config at '{}' to version {}", path, upgradeRewriter.getLatestVersion()); + + String json = GSON.toJson(object); + Files.writeString(path, json); + } + + for (Option option : config.getOptions()) { + if (option.serializedName != null && object.has(option.serializedName)) { + readIntoValue(option.value, object.get(option.serializedName)); + } + } + } catch (IOException e) { + LOGGER.error("Failed to load config from '{}'", path, e); + } + } + + public static void readIntoValue(Value value, JsonElement element) { + value.set(GSON.fromJson(element, value.getValueType())); + } + + public static void saveToPath(Config config, Path path) { + JsonObject object = new JsonObject(); + + UpgradeRewriter upgradeRewriter = config.getUpgradeRewriter(); + if (upgradeRewriter != null) { + object.addProperty("version", upgradeRewriter.getLatestVersion()); + } + + for (Option option : config.getOptions()) { + if (option.serializedName != null) { + object.add(option.serializedName, GSON.toJsonTree(option.value.get(), option.value.getValueType())); + } + } + + try { + String json = GSON.toJson(object); + Files.writeString(path, json); + } catch (IOException e) { + LOGGER.error("Failed to save config to '{}'", path, e); + } + } + + public interface UpgradeRewriter { + void upgrade(int fromVersion, JsonObject config); + + int getLatestVersion(); + } +} diff --git a/src/main/java/me/chrr/tapestry/config/NamingStrategy.java b/src/main/java/me/chrr/tapestry/config/NamingStrategy.java new file mode 100644 index 0000000..806dd8a --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/NamingStrategy.java @@ -0,0 +1,40 @@ +package me.chrr.tapestry.config; + +import org.jspecify.annotations.NullMarked; + +import java.util.Arrays; + +@NullMarked +public enum NamingStrategy { + KEEP, + SNAKE_CASE, + CAMEL_CASE; + + public String transform(String name) { + return switch (this) { + case KEEP -> name; + case SNAKE_CASE -> String.join("_", splitName(name)).toLowerCase(); + case CAMEL_CASE -> { + StringBuilder builder = new StringBuilder(); + String[] words = splitName(name); + builder.append(words[0]); + Arrays.stream(words).skip(1).forEach((word) -> builder.append(capitalize(word))); + yield builder.toString(); + } + }; + } + + private static String[] splitName(String name) { + return name + .replaceAll("[^\\p{Alnum}]+", " ") + .replaceAll("([\\p{Lower}\\d])(\\p{Upper})", "$1 $2") + .replaceAll("(\\p{Upper}+)(\\p{Upper}\\p{Lower})", "$1 $2") + .replaceAll("(\\p{Alpha})(\\d)|(\\d)(\\p{Alpha})", "$1$3 $2$4") + .trim() + .split("\\s+"); + } + + private static String capitalize(String s) { + return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); + } +} diff --git a/src/main/java/me/chrr/tapestry/config/Option.java b/src/main/java/me/chrr/tapestry/config/Option.java new file mode 100644 index 0000000..dbf61fb --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/Option.java @@ -0,0 +1,20 @@ +package me.chrr.tapestry.config; + +import me.chrr.tapestry.config.value.Value; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public class Option { + public @Nullable String serializedName; + public Value value; + + public Component displayName = Component.empty(); + public @Nullable Component header; + public boolean hidden; + + public Option(Value value) { + this.value = value; + } +} diff --git a/src/main/java/me/chrr/tapestry/config/ReflectedConfig.java b/src/main/java/me/chrr/tapestry/config/ReflectedConfig.java new file mode 100644 index 0000000..c2f0aa2 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/ReflectedConfig.java @@ -0,0 +1,206 @@ +package me.chrr.tapestry.config; + +import com.google.gson.JsonObject; +import me.chrr.tapestry.config.annotation.*; +import me.chrr.tapestry.config.value.Constraint; +import me.chrr.tapestry.config.value.TrackedValue; +import me.chrr.tapestry.config.value.Value; +import me.chrr.tapestry.config.value.VirtualValue; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.*; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Function; + +@NullMarked +public abstract class ReflectedConfig implements Config { + private final List> options = new ArrayList<>(); + + private Component title = Component.empty(); + private ConfigIo.@Nullable UpgradeRewriter upgradeRewriter = null; + + private @Nullable Path currentConfigPath = null; + private @Nullable TranslationPrefix translationPrefix = null; + + //region Value constructors + protected Value value(T defaultValue) { + //noinspection unchecked: this should be correct. + return new TrackedValue<>((Class) defaultValue.getClass(), defaultValue); + } + + protected Value map(Value value, Function aToB, Function bToA) { + V defaultValue = aToB.apply(value.getDefaultValue()); + + //noinspection unchecked: this should be correct. + return new VirtualValue<>((Class) defaultValue.getClass(), defaultValue, + () -> aToB.apply(value.get()), (v) -> value.set(bToA.apply(v))); + } + //endregion + + //region Initialization & reflection + private void reflectOptions() { + this.upgradeRewriter = reflectUpgradeRewriter(); + this.translationPrefix = getClass().getAnnotation(TranslationPrefix.class); + + // Get the default naming strategy. + NamingStrategy namingStrategy = NamingStrategy.SNAKE_CASE; + SerializeName.Strategy serializeNameStrategyAnnotation = getClass().getAnnotation(SerializeName.Strategy.class); + if (serializeNameStrategyAnnotation != null) + namingStrategy = serializeNameStrategyAnnotation.value(); + + // Get the config screen title. + this.title = getTranslatedName("title"); + + // Construct options for all public, non-static, non-transient fields. + for (Field field : getClass().getFields()) { + if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) + continue; + this.options.add(reflectOptionFromField(field, namingStrategy)); + } + } + + private ConfigIo.@Nullable UpgradeRewriter reflectUpgradeRewriter() { + ConfigIo.UpgradeRewriter upgradeRewriter = null; + + for (Method method : getClass().getDeclaredMethods()) { + UpgradeRewriter annotation = method.getAnnotation(UpgradeRewriter.class); + if (annotation == null) + continue; + + if (!Modifier.isStatic(method.getModifiers())) + throw new IllegalArgumentException("Upgrade rewriter '" + method.getName() + "' is not static"); + + if (upgradeRewriter != null) + throw new IllegalArgumentException("Config class '" + getClass().getName() + "' defines more than one upgrade rewriter"); + + method.setAccessible(true); + upgradeRewriter = new ConfigIo.UpgradeRewriter() { + @Override + public void upgrade(int fromVersion, JsonObject config) { + try { + method.invoke(null, fromVersion, config); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("Failed to invoke upgrade rewriter", e); + } + } + + @Override + public int getLatestVersion() { + return annotation.currentVersion(); + } + }; + } + + return upgradeRewriter; + } + + private Option reflectOptionFromField(Field field, NamingStrategy defaultNamingStrategy) { + try { + Class type = field.getType(); + if (!Value.class.isAssignableFrom(type)) + throw new IllegalArgumentException("All (non-transient) public fields in a config class should be Value, which " + type + " isn't"); + + Value value = (Value) field.get(this); + return reflectOptionFromValue(value, field, defaultNamingStrategy); + } catch (IllegalAccessException e) { + throw new RuntimeException("Couldn't initialize config field " + field); + } + } + + private Option reflectOptionFromValue(Value value, Field field, NamingStrategy defaultNamingStrategy) { + Option option = new Option<>(value); + + SerializeName serializeName = field.getAnnotation(SerializeName.class); + SerializeName.Strategy strategy = getClass().getAnnotation(SerializeName.Strategy.class); + NamingStrategy namingStrategy = strategy != null ? strategy.value() : defaultNamingStrategy; + + // Serialization options. + if (value instanceof TrackedValue trackedValue) { + option.serializedName = serializeName != null ? serializeName.value() : namingStrategy.transform(field.getName()); + + // Fill in some value properties if it's an enum. + Class valueType = trackedValue.getValueType(); + if (valueType.isEnum()) { + if (value.constraint == null) + value.constraint = new Constraint.Values<>(Arrays.asList(valueType.getEnumConstants())); + + if (!value.didSetTextProvider) + value.textProvider = (v) -> getTranslatedName( + "value." + namingStrategy.transform(valueType.getSimpleName()) + + "." + namingStrategy.transform(v.toString())); + } + } + + // GUI-specific options. + if (field.isAnnotationPresent(Hidden.class)) { + option.hidden = true; + } else { + option.displayName = getTranslatedName("option." + namingStrategy.transform(field.getName())); + } + + Header header = field.getAnnotation(Header.class); + if (header != null) + option.header = getTranslatedName("category." + header.value()); + + return option; + } + + private Component getTranslatedName(String key) { + if (this.translationPrefix == null) { + return Component.literal(key); + } else { + return Component.translatable(this.translationPrefix.value() + "." + key); + } + } + //endregion + + //region Saving & loading + @Override + public void save() { + ConfigIo.saveToPath(this, Objects.requireNonNull(this.currentConfigPath)); + } + + public static T load(ConfigEnvironment environment, Class configClass, String file, List aliases) { + try { + Constructor constructor = configClass.getConstructor(); + constructor.setAccessible(true); + + T config = constructor.newInstance(); + ((ReflectedConfig) config).reflectOptions(); + + Path confDir = environment.getConfigDir(); + Path configFile = confDir.resolve(file); + ((ReflectedConfig) config).currentConfigPath = configFile; + + ConfigIo.loadFromPathOrSaveDefault(config, configFile, + aliases.stream().map(confDir::resolve).toList()); + + return config; + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("Config class '" + configClass.getName() + "' does not have a default constructor", e); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new IllegalArgumentException("Config class '" + configClass.getName() + "' could not be instantiated", e); + } + } + //endregion + + //region Interface implementations + @Override + public Collection> getOptions() { + return this.options; + } + + @Override + public Component getTitle() { + return this.title; + } + + @Override + public ConfigIo.@Nullable UpgradeRewriter getUpgradeRewriter() { + return this.upgradeRewriter; + } + //endregion +} diff --git a/src/main/java/me/chrr/tapestry/config/annotation/Header.java b/src/main/java/me/chrr/tapestry/config/annotation/Header.java new file mode 100644 index 0000000..8ee7a3e --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/annotation/Header.java @@ -0,0 +1,15 @@ +package me.chrr.tapestry.config.annotation; + +import org.jspecify.annotations.NullMarked; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@NullMarked +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Header { + String value(); +} diff --git a/src/main/java/me/chrr/tapestry/config/annotation/Hidden.java b/src/main/java/me/chrr/tapestry/config/annotation/Hidden.java new file mode 100644 index 0000000..c751fb0 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/annotation/Hidden.java @@ -0,0 +1,14 @@ +package me.chrr.tapestry.config.annotation; + +import org.jspecify.annotations.NullMarked; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@NullMarked +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Hidden { +} diff --git a/src/main/java/me/chrr/tapestry/config/annotation/SerializeName.java b/src/main/java/me/chrr/tapestry/config/annotation/SerializeName.java new file mode 100644 index 0000000..1c3bea5 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/annotation/SerializeName.java @@ -0,0 +1,22 @@ +package me.chrr.tapestry.config.annotation; + +import me.chrr.tapestry.config.NamingStrategy; +import org.jspecify.annotations.NullMarked; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@NullMarked +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SerializeName { + String value(); + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE, ElementType.FIELD}) + @interface Strategy { + NamingStrategy value(); + } +} diff --git a/src/main/java/me/chrr/tapestry/config/annotation/TranslationPrefix.java b/src/main/java/me/chrr/tapestry/config/annotation/TranslationPrefix.java new file mode 100644 index 0000000..ec5a024 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/annotation/TranslationPrefix.java @@ -0,0 +1,15 @@ +package me.chrr.tapestry.config.annotation; + +import org.jspecify.annotations.NullMarked; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@NullMarked +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface TranslationPrefix { + String value(); +} diff --git a/src/main/java/me/chrr/tapestry/config/annotation/UpgradeRewriter.java b/src/main/java/me/chrr/tapestry/config/annotation/UpgradeRewriter.java new file mode 100644 index 0000000..b162f79 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/annotation/UpgradeRewriter.java @@ -0,0 +1,15 @@ +package me.chrr.tapestry.config.annotation; + +import org.jspecify.annotations.NullMarked; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@NullMarked +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface UpgradeRewriter { + int currentVersion(); +} diff --git a/src/main/java/me/chrr/tapestry/config/gui/IconButton.java b/src/main/java/me/chrr/tapestry/config/gui/IconButton.java new file mode 100644 index 0000000..d44a21b --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/gui/IconButton.java @@ -0,0 +1,52 @@ +package me.chrr.tapestry.config.gui; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractButton; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.input.InputWithModifiers; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.util.ARGB; +import net.minecraft.util.CommonColors; +import org.jspecify.annotations.NullMarked; + +import java.util.function.Consumer; + +@NullMarked +public class IconButton extends AbstractButton { + private final Consumer onPress; + private final Identifier icon; + + public IconButton(int x, int y, int width, int height, Component tooltip, Identifier icon, Consumer onPress) { + super(x, y, width, height, tooltip); + this.onPress = onPress; + this.icon = icon; + + setTooltip(Tooltip.create(tooltip)); + } + + @Override + public void onPress(InputWithModifiers input) { + this.onPress.accept(this); + } + + @Override + protected void renderContents(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + this.renderDefaultSprite(guiGraphics); + + int color = this.isActive() ? CommonColors.WHITE : CommonColors.LIGHT_GRAY; + int shadow = ARGB.scaleRGB(color, 0.25F); + + int x = getX() + getWidth() / 2 - 16 / 2; + int y = getY() + getHeight() / 2 - 16 / 2; + guiGraphics.blit(RenderPipelines.GUI_TEXTURED, icon, x + 1, y + 1, 0, 0, 16, 16, 16, 16, shadow); + guiGraphics.blit(RenderPipelines.GUI_TEXTURED, icon, x, y, 0, 0, 16, 16, 16, 16, color); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput output) { + this.defaultButtonNarrationText(output); + } +} diff --git a/src/main/java/me/chrr/tapestry/config/gui/OptionList.java b/src/main/java/me/chrr/tapestry/config/gui/OptionList.java new file mode 100644 index 0000000..9b815e9 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/gui/OptionList.java @@ -0,0 +1,142 @@ +package me.chrr.tapestry.config.gui; + +import me.chrr.tapestry.config.Option; +import me.chrr.tapestry.config.gui.widget.*; +import me.chrr.tapestry.config.value.Constraint; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.ContainerObjectSelectionList; +import net.minecraft.client.gui.components.StringWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import org.jspecify.annotations.NullMarked; + +import java.util.List; + +@NullMarked +public class OptionList extends ContainerObjectSelectionList { + private final boolean showHeaderSeparator; + + public OptionList(Minecraft minecraft, boolean showHeaderSeparator, int width, int height, int y) { + super(minecraft, width, height, y, 25); + + this.showHeaderSeparator = showHeaderSeparator; + this.centerListVertically = false; + } + + public void addHeader(Component text) { + int padding = this.children().isEmpty() ? 8 : 16; + int height = padding + OptionList.this.minecraft.font.lineHeight; + this.addEntry(new HeaderEntry(text), height); + } + + public OptionProxy addOption(Option option) { + OptionProxy proxy = new OptionProxy<>(option); + this.addEntry(new OptionEntry<>(proxy)); + return proxy; + } + + @Override + public int getRowWidth() { + return 310; + } + + @Override + protected void renderListSeparators(GuiGraphics guiGraphics) { + if (this.showHeaderSeparator) { + Identifier headerSeparator = this.minecraft.level == null ? Screen.HEADER_SEPARATOR : Screen.INWORLD_HEADER_SEPARATOR; + guiGraphics.blit(RenderPipelines.GUI_TEXTURED, headerSeparator, this.getX(), this.getY() - 2, 0f, 0f, this.getWidth(), 2, 32, 2); + } + + Identifier footerSeparator = this.minecraft.level == null ? Screen.FOOTER_SEPARATOR : Screen.INWORLD_FOOTER_SEPARATOR; + guiGraphics.blit(RenderPipelines.GUI_TEXTURED, footerSeparator, this.getX(), this.getBottom(), 0f, 0f, this.getWidth(), 2, 32, 2); + } + + private static OptionWidget getWidgetForProxy(OptionProxy optionProxy) { + Class valueClass = optionProxy.option.value.getValueType(); + + if (valueClass == boolean.class || valueClass == Boolean.class) { + return unsafeCast(new BooleanOptionWidget(unsafeCast(optionProxy))); + } else if (optionProxy.option.value.constraint instanceof Constraint.Range range && range.step() != null) { + if ((valueClass == int.class || valueClass == Integer.class)) { + return unsafeCast(new SliderOptionWidget.Int(unsafeCast(optionProxy), unsafeCast(range))); + } else if ((valueClass == float.class || valueClass == Float.class)) { + return unsafeCast(new SliderOptionWidget.Float(unsafeCast(optionProxy), unsafeCast(range))); + } else { + throw new IllegalArgumentException("Range constraint can't be applied to a value of type " + valueClass); + } + } else if (optionProxy.option.value.constraint instanceof Constraint.Values(List values)) { + return new EnumOptionWidget<>(optionProxy, values); + } else { + return new ReadOnlyOptionWidget<>(optionProxy); + } + } + + @SuppressWarnings("unchecked") + private static T unsafeCast(V value) { + return (T) value; + } + + protected abstract static class Entry extends ContainerObjectSelectionList.Entry { + } + + protected class HeaderEntry extends Entry { + private final StringWidget widget; + + public HeaderEntry(Component text) { + this.widget = new StringWidget(text, minecraft.font); + } + + public List narratables() { + return List.of(this.widget); + } + + public void renderContent(GuiGraphics graphics, int x, int y, boolean bl, float delta) { + this.widget.setPosition(this.getContentX() + 2, this.getContentBottom() - minecraft.font.lineHeight); + this.widget.render(graphics, x, y, delta); + } + + public List children() { + return List.of(this.widget); + } + } + + protected static class OptionEntry extends Entry { + private final OptionWidget widget; + private final OptionProxy optionProxy; + private final IconButton reset; + + public OptionEntry(OptionProxy optionProxy) { + this.widget = getWidgetForProxy(optionProxy); + this.optionProxy = optionProxy; + + this.reset = new IconButton(0, 0, 0, 0, + Component.translatable("text.tapestry.config.reset"), + Identifier.fromNamespaceAndPath("tapestry-config", "textures/gui/reset.png"), + (button) -> optionProxy.reset()); + } + + public List narratables() { + return List.of(this.widget, this.reset); + } + + public void renderContent(GuiGraphics graphics, int x, int y, boolean isHovering, float delta) { + this.widget.setPosition(this.getContentX(), this.getContentY()); + this.widget.setSize(this.getContentWidth() - this.getContentHeight() - 4, this.getContentHeight()); + this.widget.render(graphics, x, y, delta); + + this.reset.active = this.optionProxy.isChanged(); + this.reset.setPosition(this.getContentRight() - this.getContentHeight(), this.getContentY()); + this.reset.setSize(this.getContentHeight(), this.getContentHeight()); + this.reset.render(graphics, x, y, delta); + } + + public List children() { + return List.of(this.widget, this.reset); + } + } +} diff --git a/src/main/java/me/chrr/tapestry/config/gui/OptionProxy.java b/src/main/java/me/chrr/tapestry/config/gui/OptionProxy.java new file mode 100644 index 0000000..ac74dd2 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/gui/OptionProxy.java @@ -0,0 +1,27 @@ +package me.chrr.tapestry.config.gui; + +import me.chrr.tapestry.config.Option; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class OptionProxy { + public final Option option; + public T value; + + public OptionProxy(Option option) { + this.option = option; + this.value = option.value.get(); + } + + public void apply() { + this.option.value.set(this.value); + } + + public void reset() { + this.value = this.option.value.getDefaultValue(); + } + + public boolean isChanged() { + return this.value != this.option.value.getDefaultValue(); + } +} diff --git a/src/main/java/me/chrr/tapestry/config/gui/TapestryConfigScreen.java b/src/main/java/me/chrr/tapestry/config/gui/TapestryConfigScreen.java new file mode 100644 index 0000000..89169fe --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/gui/TapestryConfigScreen.java @@ -0,0 +1,140 @@ +package me.chrr.tapestry.config.gui; + +import me.chrr.tapestry.config.Config; +import me.chrr.tapestry.config.Option; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.tabs.Tab; +import net.minecraft.client.gui.components.tabs.TabManager; +import net.minecraft.client.gui.components.tabs.TabNavigationBar; +import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; +import net.minecraft.client.gui.layouts.LinearLayout; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +@NullMarked +public class TapestryConfigScreen extends Screen { + private final Config[] configs; + private final Screen parent; + + private final List> proxies = new ArrayList<>(); + + private final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this); + private final TabManager tabManager = new TabManager(this::addRenderableWidget, this::removeWidget); + + private @Nullable TabNavigationBar tabNavigationBar; + private @Nullable OptionList optionList; + + public TapestryConfigScreen(Screen parent, Config... configs) { + super(Component.empty()); + assert configs.length > 0; + + this.configs = configs; + this.parent = parent; + } + + @Override + protected void init() { + // Initialise the actual option lists. + ConfigTab[] tabs = new ConfigTab[this.configs.length]; + for (int i = 0; i < this.configs.length; i++) { + OptionList list = createOptionList(this.configs[i], this.width, + this.layout.getContentHeight(), this.layout.getHeaderHeight()); + tabs[i] = new ConfigTab(this.configs[i], list); + } + + if (tabs.length == 1) { + // If we have only a single config, just show a simple title header. + this.layout.addTitleHeader(tabs[0].getTabTitle(), this.font); + this.optionList = this.layout.addToContents(createOptionList(tabs[0].config, this.width, + this.layout.getContentHeight(), this.layout.getHeaderHeight())); + } else { + // If we have more than one config, show them as tabs. + this.tabNavigationBar = this.addRenderableWidget( + TabNavigationBar.builder(this.tabManager, this.width).addTabs(tabs).build()); + this.tabNavigationBar.selectTab(0, false); + } + + // Add the footer cancel and done buttons. + LinearLayout footerLayout = this.layout.addToFooter(LinearLayout.horizontal().spacing(8)); + footerLayout.addChild(Button.builder(CommonComponents.GUI_CANCEL, (button) -> this.onClose()).build()); + footerLayout.addChild(Button.builder(CommonComponents.GUI_DONE, (button) -> this.saveAndClose()).build()); + + this.layout.visitWidgets(this::addRenderableWidget); + this.repositionElements(); + } + + @Override + protected void repositionElements() { + if (this.optionList != null) { + this.layout.arrangeElements(); + this.optionList.updateSize(this.width, this.layout); + } else if (this.tabNavigationBar != null) { + this.tabNavigationBar.setWidth(this.width); + this.tabNavigationBar.arrangeElements(); + + int headerHeight = this.tabNavigationBar.getRectangle().bottom(); + ScreenRectangle screenRectangle = new ScreenRectangle(0, headerHeight, + this.width, this.height - headerHeight - this.layout.getFooterHeight()); + this.tabManager.setTabArea(screenRectangle); + this.layout.setHeaderHeight(headerHeight); + this.layout.arrangeElements(); + } + } + + private OptionList createOptionList(Config config, int width, int height, int y) { + OptionList list = new OptionList(this.minecraft, this.configs.length <= 1, width, height, y); + for (Option option : config.getOptions()) { + if (option.header != null) + list.addHeader(option.header); + + if (!option.hidden) + this.proxies.add(list.addOption(option)); + } + + return list; + } + + public void saveAndClose() { + this.proxies.forEach(OptionProxy::apply); + for (Config config : this.configs) + config.save(); + + this.onClose(); + } + + public void onClose() { + this.minecraft.setScreen(this.parent); + } + + public record ConfigTab(Config config, OptionList list) implements Tab { + @Override + public Component getTabTitle() { + return this.config.getTitle(); + } + + @Override + public Component getTabExtraNarration() { + return Component.empty(); + } + + @Override + public void visitChildren(Consumer consumer) { + consumer.accept(this.list); + } + + @Override + public void doLayout(ScreenRectangle rectangle) { + this.list.updateSizeAndPosition(rectangle.width(), rectangle.height(), rectangle.top()); + } + } +} + diff --git a/src/main/java/me/chrr/tapestry/config/gui/widget/BooleanOptionWidget.java b/src/main/java/me/chrr/tapestry/config/gui/widget/BooleanOptionWidget.java new file mode 100644 index 0000000..8390e6b --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/gui/widget/BooleanOptionWidget.java @@ -0,0 +1,40 @@ +package me.chrr.tapestry.config.gui.widget; + +import me.chrr.tapestry.config.gui.OptionProxy; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.input.InputWithModifiers; +import net.minecraft.util.ARGB; +import net.minecraft.util.CommonColors; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class BooleanOptionWidget extends OptionWidget.Clickable { + public BooleanOptionWidget(OptionProxy optionProxy) { + super(optionProxy); + } + + @Override + public void onPress(InputWithModifiers input) { + optionProxy.value = !optionProxy.value; + } + + @Override + protected void renderOptionWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + int boxSize = this.getHeight() - (2 + 4) * 2; + int boxX = this.getRight() - 2 - 4 - boxSize; + int boxY = this.getY() + 2 + 4; + + int color = CommonColors.WHITE; + int shadow = ARGB.scaleRGB(color, 0.25F); + + this.renderOptionLabel(graphics, boxX - this.getX()); + + graphics.renderOutline(boxX + 1, boxY + 1, boxSize, boxSize, shadow); + graphics.renderOutline(boxX, boxY, boxSize, boxSize, color); + + if (optionProxy.value) { + graphics.fill(boxX + 3, boxY + 3, boxX + boxSize - 1, boxY + boxSize - 1, shadow); + graphics.fill(boxX + 2, boxY + 2, boxX + boxSize - 2, boxY + boxSize - 2, color); + } + } +} diff --git a/src/main/java/me/chrr/tapestry/config/gui/widget/EnumOptionWidget.java b/src/main/java/me/chrr/tapestry/config/gui/widget/EnumOptionWidget.java new file mode 100644 index 0000000..8cefaa6 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/gui/widget/EnumOptionWidget.java @@ -0,0 +1,36 @@ +package me.chrr.tapestry.config.gui.widget; + +import me.chrr.tapestry.config.gui.OptionProxy; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.input.InputWithModifiers; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.NullMarked; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@NullMarked +public class EnumOptionWidget extends OptionWidget.Clickable { + private final List values; + private final Map nameByValue = new HashMap<>(); + + public EnumOptionWidget(OptionProxy optionProxy, List values) { + super(optionProxy); + + this.values = values; + for (T value : values) + this.nameByValue.put(value, optionProxy.option.value.textProvider.apply(value)); + } + + @Override + public void onPress(InputWithModifiers input) { + optionProxy.value = values.get((values.indexOf(optionProxy.value) + 1) % values.size()); + } + + @Override + protected void renderOptionWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + this.renderOptionLabel(graphics, this.getWidth()); + this.renderValueLabel(graphics, 0, this.getWidth(), nameByValue.get(optionProxy.value)); + } +} diff --git a/src/main/java/me/chrr/tapestry/config/gui/widget/OptionWidget.java b/src/main/java/me/chrr/tapestry/config/gui/widget/OptionWidget.java new file mode 100644 index 0000000..8382eab --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/gui/widget/OptionWidget.java @@ -0,0 +1,105 @@ +package me.chrr.tapestry.config.gui.widget; + +import me.chrr.tapestry.config.gui.OptionProxy; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ActiveTextCollector; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.WidgetSprites; +import net.minecraft.client.gui.components.WidgetTooltipHolder; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.input.InputWithModifiers; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.util.ARGB; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public abstract class OptionWidget extends AbstractWidget { + private static final WidgetSprites SPRITES = new WidgetSprites( + Identifier.withDefaultNamespace("widget/button"), + Identifier.withDefaultNamespace("widget/button_disabled"), + Identifier.withDefaultNamespace("widget/button_highlighted") + ); + + public final OptionProxy optionProxy; + public final WidgetTooltipHolder customTooltip; + + public OptionWidget(OptionProxy optionProxy) { + super(0, 0, 0, 0, optionProxy.option.displayName); + + this.optionProxy = optionProxy; + this.customTooltip = new WidgetTooltipHolder(); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + this.defaultButtonNarrationText(narrationElementOutput); + } + + protected void renderOptionLabel(GuiGraphics graphics, int availableWidth) { + ActiveTextCollector textCollector = graphics.textRendererForWidget(this, GuiGraphics.HoveredTextEffects.NONE); + textCollector.acceptScrolling(this.getMessage(), + this.getX() + 2 + 4, this.getX() + 2 + 4, this.getX() + availableWidth - 2 - 4, + this.getY() + 2, this.getBottom() - 2); + } + + protected void renderValueLabel(GuiGraphics graphics, int rightOffset, int availableWidth) { + this.renderValueLabel(graphics, rightOffset, availableWidth, optionProxy.option.value.textProvider.apply(optionProxy.value)); + } + + protected void renderValueLabel(GuiGraphics graphics, int rightOffset, int availableWidth, Component value) { + ActiveTextCollector textCollector = graphics.textRendererForWidget(this, GuiGraphics.HoveredTextEffects.NONE); + textCollector.acceptScrolling(value, + this.getRight() - rightOffset - 2 - 4, + this.getRight() - rightOffset - availableWidth + 2 + 4, + this.getRight() - rightOffset - 2 - 4, + this.getY() + 2, this.getBottom() - 2); + } + + @Override + protected void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + graphics.blitSprite( + RenderPipelines.GUI_TEXTURED, SPRITES.get(this.active, this.isHoveredOrFocused()), + this.getX(), this.getY(), this.getWidth(), this.getHeight(), + ARGB.white(this.alpha)); + this.renderOptionWidget(graphics, mouseX, mouseY, partialTick); + } + + protected abstract void renderOptionWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta); + + public static abstract class Clickable extends OptionWidget { + public Clickable(OptionProxy optionProxy) { + super(optionProxy); + } + + @Override + public void onClick(MouseButtonEvent event, boolean isDoubleClick) { + this.onPress(event); + } + + @Override + public boolean keyPressed(KeyEvent event) { + if (!this.isActive()) { + return false; + } else if (event.isSelection()) { + this.playDownSound(Minecraft.getInstance().getSoundManager()); + this.onPress(event); + return true; + } else { + return false; + } + } + + @Override + protected void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + super.renderWidget(graphics, mouseX, mouseY, partialTick); + this.handleCursor(graphics); + } + + public abstract void onPress(InputWithModifiers input); + } +} diff --git a/src/main/java/me/chrr/tapestry/config/gui/widget/ReadOnlyOptionWidget.java b/src/main/java/me/chrr/tapestry/config/gui/widget/ReadOnlyOptionWidget.java new file mode 100644 index 0000000..dc332a2 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/gui/widget/ReadOnlyOptionWidget.java @@ -0,0 +1,20 @@ +package me.chrr.tapestry.config.gui.widget; + +import me.chrr.tapestry.config.gui.OptionProxy; +import net.minecraft.client.gui.GuiGraphics; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class ReadOnlyOptionWidget extends OptionWidget { + public ReadOnlyOptionWidget(OptionProxy optionProxy) { + super(optionProxy); + this.active = false; + } + + @Override + protected void renderOptionWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + this.renderOptionLabel(graphics, this.getWidth()); + this.renderValueLabel(graphics, 0, this.getWidth()); + this.handleCursor(graphics); + } +} diff --git a/src/main/java/me/chrr/tapestry/config/gui/widget/SliderOptionWidget.java b/src/main/java/me/chrr/tapestry/config/gui/widget/SliderOptionWidget.java new file mode 100644 index 0000000..66c7651 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/gui/widget/SliderOptionWidget.java @@ -0,0 +1,160 @@ +package me.chrr.tapestry.config.gui.widget; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.platform.cursor.CursorTypes; +import me.chrr.tapestry.config.gui.OptionProxy; +import me.chrr.tapestry.config.value.Constraint; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.util.ARGB; +import net.minecraft.util.CommonColors; +import org.jspecify.annotations.NullMarked; + +import java.util.Objects; + +@NullMarked +public abstract class SliderOptionWidget extends OptionWidget { + private static final int SLIDER_WIDTH = 96; + + private int sliderMinX = 0; + private int sliderMaxX = 0; + private boolean isSliding = false; + + public final N min; + public final N max; + public final N step; + + public SliderOptionWidget(OptionProxy optionProxy, Constraint.Range range) { + super(optionProxy); + + this.min = range.min(); + this.max = range.max(); + this.step = Objects.requireNonNull(range.step()); + } + + @Override + public boolean keyPressed(KeyEvent event) { + if (this.isFocused()) { + switch (event.key()) { + case InputConstants.KEY_LEFT -> { + optionProxy.value = incrementBySteps(optionProxy.value, -1); + return true; + } + case InputConstants.KEY_RIGHT -> { + optionProxy.value = incrementBySteps(optionProxy.value, 1); + return true; + } + } + } + + return false; + } + + @Override + public void onClick(MouseButtonEvent event, boolean isDoubleClick) { + this.isSliding = event.button() == InputConstants.MOUSE_BUTTON_LEFT + && event.x() >= sliderMinX && event.x() <= sliderMaxX; + this.updateSlider(event); + } + + @Override + public void onRelease(MouseButtonEvent event) { + this.isSliding = false; + } + + @Override + protected void onDrag(MouseButtonEvent event, double mouseX, double mouseY) { + this.updateSlider(event); + } + + protected void updateSlider(MouseButtonEvent event) { + if (!isSliding || event.button() != InputConstants.MOUSE_BUTTON_LEFT) + return; + + float progress = Math.clamp(((float) event.x() - sliderMinX) / SLIDER_WIDTH, 0f, 1f); + optionProxy.value = getValueFromProgress(progress); + } + + @Override + protected void renderOptionWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + int sliderWidth = this.isHoveredOrFocused() ? SLIDER_WIDTH : SLIDER_WIDTH / 4; + int availableWidth = this.getWidth() - sliderWidth - 4; + this.renderOptionLabel(graphics, availableWidth); + this.renderValueLabel(graphics, sliderWidth + 4, availableWidth); + + int color = CommonColors.WHITE; + int shadow = ARGB.scaleRGB(color, 0.25F); + + sliderMaxX = this.getRight() - 2 - 4; + sliderMinX = sliderMaxX - sliderWidth; + int sliderY = this.getY() + this.getHeight() / 2; + + int thumbWidth = 2; + int thumbHeight = this.getHeight() - (2 + 4) * 2; + float progress = Math.clamp(getProgressFromValue(optionProxy.value), 0f, 1f); + int thumbX = sliderMinX + (int) (sliderWidth * progress) - thumbWidth / 2; + int thumbY = this.getY() + 2 + 4; + + graphics.fill(sliderMinX + 1, sliderY + 1, sliderMaxX + 1, sliderY + 2, shadow); + graphics.fill(sliderMinX, sliderY, sliderMaxX, sliderY + 1, color); + + graphics.fill(thumbX + 1, thumbY + 1, thumbX + thumbWidth + 1, thumbY + thumbHeight + 1, shadow); + graphics.fill(thumbX, thumbY, thumbX + thumbWidth, thumbY + thumbHeight, color); + + if (this.isSliding) { + graphics.requestCursor(CursorTypes.RESIZE_EW); + } else if (this.isHovered() && mouseX >= sliderMinX && mouseX <= sliderMaxX) { + graphics.requestCursor(this.isActive() ? CursorTypes.POINTING_HAND : CursorTypes.NOT_ALLOWED); + } + } + + protected abstract float getProgressFromValue(N value); + + protected abstract N getValueFromProgress(float progress); + + protected abstract N incrementBySteps(N value, int steps); + + + public static class Int extends SliderOptionWidget { + public Int(OptionProxy optionProxy, Constraint.Range range) { + super(optionProxy, range); + } + + @Override + protected float getProgressFromValue(Integer value) { + return ((float) value - this.min) / ((float) this.max - this.min); + } + + @Override + protected Integer getValueFromProgress(float progress) { + return (int) (this.min + progress * (this.max - this.min)) / step * step; + } + + @Override + protected Integer incrementBySteps(Integer value, int steps) { + return Math.clamp(value + (long) this.step * steps, this.min, this.max); + } + } + + public static class Float extends SliderOptionWidget { + public Float(OptionProxy optionProxy, Constraint.Range range) { + super(optionProxy, range); + } + + @Override + protected float getProgressFromValue(java.lang.Float value) { + return (value - this.min) / (this.max - this.min); + } + + @Override + protected java.lang.Float getValueFromProgress(float progress) { + return Math.round((this.min + progress * (this.max - this.min)) / step) * step; + } + + @Override + protected java.lang.Float incrementBySteps(java.lang.Float value, int steps) { + return Math.clamp(value + this.step * steps, this.min, this.max); + } + } +} diff --git a/src/main/java/me/chrr/tapestry/config/value/Constraint.java b/src/main/java/me/chrr/tapestry/config/value/Constraint.java new file mode 100644 index 0000000..cee4be1 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/value/Constraint.java @@ -0,0 +1,15 @@ +package me.chrr.tapestry.config.value; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +@NullMarked +public sealed interface Constraint { + record Range(T min, T max, @Nullable T step) implements Constraint { + } + + record Values(List values) implements Constraint { + } +} diff --git a/src/main/java/me/chrr/tapestry/config/value/TrackedValue.java b/src/main/java/me/chrr/tapestry/config/value/TrackedValue.java new file mode 100644 index 0000000..662222d --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/value/TrackedValue.java @@ -0,0 +1,36 @@ +package me.chrr.tapestry.config.value; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class TrackedValue extends Value { + private final Class type; + private final T defaultValue; + public T value; + + public TrackedValue(Class type, T defaultValue) { + this.type = type; + this.defaultValue = defaultValue; + this.value = defaultValue; + } + + @Override + public void set(T value) { + this.value = value; + } + + @Override + public T getDefaultValue() { + return this.defaultValue; + } + + @Override + public T get() { + return this.value; + } + + @Override + public Class getValueType() { + return this.type; + } +} diff --git a/src/main/java/me/chrr/tapestry/config/value/Value.java b/src/main/java/me/chrr/tapestry/config/value/Value.java new file mode 100644 index 0000000..8ae5d40 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/value/Value.java @@ -0,0 +1,38 @@ +package me.chrr.tapestry.config.value; + +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.function.Function; +import java.util.function.Supplier; + +@NullMarked +public abstract class Value implements Supplier { + public Function textProvider = (v) -> Component.literal(v.toString()); + public @Nullable Constraint constraint; + + public boolean didSetTextProvider = false; + + public Value range(T min, T max) { + this.constraint = new Constraint.Range<>(min, max, null); + return this; + } + + public Value range(T min, T max, T step) { + this.constraint = new Constraint.Range<>(min, max, step); + return this; + } + + public Value textProvider(Function f) { + this.didSetTextProvider = true; + this.textProvider = f; + return this; + } + + public abstract void set(T value); + + public abstract T getDefaultValue(); + + public abstract Class getValueType(); +} diff --git a/src/main/java/me/chrr/tapestry/config/value/VirtualValue.java b/src/main/java/me/chrr/tapestry/config/value/VirtualValue.java new file mode 100644 index 0000000..f25fb64 --- /dev/null +++ b/src/main/java/me/chrr/tapestry/config/value/VirtualValue.java @@ -0,0 +1,41 @@ +package me.chrr.tapestry.config.value; + +import org.jspecify.annotations.NullMarked; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +@NullMarked +public class VirtualValue extends Value { + private final Class type; + private final T defaultValue; + private final Supplier getter; + private final Consumer setter; + + public VirtualValue(Class type, T defaultValue, Supplier getter, Consumer setter) { + this.type = type; + this.defaultValue = defaultValue; + this.getter = getter; + this.setter = setter; + } + + @Override + public void set(T value) { + this.setter.accept(value); + } + + @Override + public T getDefaultValue() { + return this.defaultValue; + } + + @Override + public T get() { + return this.getter.get(); + } + + @Override + public Class getValueType() { + return this.type; + } +} diff --git a/common/src/main/resources/assets/scribble/banner.png b/src/main/resources/assets/scribble/banner.png similarity index 100% rename from common/src/main/resources/assets/scribble/banner.png rename to src/main/resources/assets/scribble/banner.png diff --git a/common/src/main/resources/assets/scribble/icon.png b/src/main/resources/assets/scribble/icon.png similarity index 100% rename from common/src/main/resources/assets/scribble/icon.png rename to src/main/resources/assets/scribble/icon.png diff --git a/common/src/main/resources/assets/scribble/lang/de_de.json b/src/main/resources/assets/scribble/lang/de_de.json similarity index 100% rename from common/src/main/resources/assets/scribble/lang/de_de.json rename to src/main/resources/assets/scribble/lang/de_de.json diff --git a/common/src/main/resources/assets/scribble/lang/en_us.json b/src/main/resources/assets/scribble/lang/en_us.json similarity index 89% rename from common/src/main/resources/assets/scribble/lang/en_us.json rename to src/main/resources/assets/scribble/lang/en_us.json index 2643ed6..9f71b89 100644 --- a/common/src/main/resources/assets/scribble/lang/en_us.json +++ b/src/main/resources/assets/scribble/lang/en_us.json @@ -18,6 +18,7 @@ "config.scribble.value.paste_behavior.overflow": "Allow overflow to new pages", "config.scribble.category.miscellaneous": "Miscellaneous", "config.scribble.option.open_vanilla_book_screen_on_shift": "Open vanilla GUIs when holding SHIFT", + "text.tapestry.config.reset": "Reset to default value", "text.scribble.action.delete_page": "Delete this page", "text.scribble.action.insert_new_page": "Insert new page\nbefore this one", "text.scribble.action.insert_new_page_here": "Insert new page here", @@ -30,10 +31,6 @@ "text.scribble.overwrite_warning.description": "The current contents of this book will be lost!", "text.scribble.quit_without_saving.title": "Are you sure you want to quit without saving?", "text.scribble.quit_without_saving.description": "The changes made to this book will be lost!", - "text.scribble.error.save_book.title": "Something went wrong while saving the book!", - "text.scribble.error.load_book.title": "Something went wrong while loading the book!", - "text.scribble.error.load_book.subtitle": "Double-check if the file you're loading is correct.", - "text.scribble.error.summary_below": "The full error can be found in the log files, summary below.", "text.scribble.modifier.bold": "Bold", "text.scribble.modifier.italic": "Italic", "text.scribble.modifier.underline": "Underline", diff --git a/common/src/main/resources/assets/scribble/lang/es_mx.json b/src/main/resources/assets/scribble/lang/es_mx.json similarity index 100% rename from common/src/main/resources/assets/scribble/lang/es_mx.json rename to src/main/resources/assets/scribble/lang/es_mx.json diff --git a/common/src/main/resources/assets/scribble/lang/ru_ru.json b/src/main/resources/assets/scribble/lang/ru_ru.json similarity index 100% rename from common/src/main/resources/assets/scribble/lang/ru_ru.json rename to src/main/resources/assets/scribble/lang/ru_ru.json diff --git a/common/src/main/resources/assets/scribble/lang/tr_tr.json b/src/main/resources/assets/scribble/lang/tr_tr.json similarity index 100% rename from common/src/main/resources/assets/scribble/lang/tr_tr.json rename to src/main/resources/assets/scribble/lang/tr_tr.json diff --git a/common/src/main/resources/assets/scribble/lang/zh_cn.json b/src/main/resources/assets/scribble/lang/zh_cn.json similarity index 100% rename from common/src/main/resources/assets/scribble/lang/zh_cn.json rename to src/main/resources/assets/scribble/lang/zh_cn.json diff --git a/common/src/main/resources/assets/scribble/lang/zh_tw.json b/src/main/resources/assets/scribble/lang/zh_tw.json similarity index 100% rename from common/src/main/resources/assets/scribble/lang/zh_tw.json rename to src/main/resources/assets/scribble/lang/zh_tw.json diff --git a/common/src/main/resources/assets/scribble/textures/gui/book_2.png b/src/main/resources/assets/scribble/textures/gui/book_2.png similarity index 100% rename from common/src/main/resources/assets/scribble/textures/gui/book_2.png rename to src/main/resources/assets/scribble/textures/gui/book_2.png diff --git a/common/src/main/resources/assets/scribble/textures/gui/scribble_widgets.png b/src/main/resources/assets/scribble/textures/gui/scribble_widgets.png similarity index 100% rename from common/src/main/resources/assets/scribble/textures/gui/scribble_widgets.png rename to src/main/resources/assets/scribble/textures/gui/scribble_widgets.png diff --git a/src/main/resources/assets/tapestry-config/textures/gui/reset.png b/src/main/resources/assets/tapestry-config/textures/gui/reset.png new file mode 100644 index 0000000000000000000000000000000000000000..bec6b7f86a4e2bd3bef62f69638350f155ed22d4 GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!D3?x-;bCrM;V{wqX6XVU3I`u#fXMsm#F#`j) zFbFd;%$g$s6l4qV32_C||NsAAdUO6=Ad9Ia$S;`TfAmD9tw5flr;B4q#hm1ngajA2 tHM=EmE|IutDJjX~(BauPvCw`K6N64I6Qc (Lnet/minecraft/client/gui/Font;IIIILnet/minecraft/network/chat/Component;Lnet/minecraft/network/chat/Component;IZIZZ)V#accessible field net/minecraft/client/gui/widget/EditBoxWidget editBox Lnet/minecraft/client/gui/EditBox; diff --git a/common/src/main/resources/scribble.mixins.json b/src/main/resources/scribble.mixins.json similarity index 100% rename from common/src/main/resources/scribble.mixins.json rename to src/main/resources/scribble.mixins.json diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts index 8628a81..85648c1 100644 --- a/stonecutter.gradle.kts +++ b/stonecutter.gradle.kts @@ -1,26 +1,19 @@ plugins { id("dev.kikugie.stonecutter") - id("me.chrr.tapestry.gradle") version "1.1.0" apply false + id("dev.architectury.loom") version "1.13-SNAPSHOT" apply false + id("architectury-plugin") version "3.4-SNAPSHOT" apply false + id("com.gradleup.shadow") version "9.3.0" apply false + id("me.modmuss50.mod-publish-plugin") version "1.1.0" apply false } -stonecutter active "26.1" /* [SC] DO NOT EDIT */ +stonecutter active "1.21.11" /* [SC] DO NOT EDIT */ for (node in stonecutter.tree.nodes) { if (node.metadata != stonecutter.current || node.branch.id.isEmpty()) continue val loader = node.branch.id.replaceFirstChar { it.uppercase() } node.project.tasks.register("runActive$loader") { - description = "Run the client for the active version using $loader." - group = "project" - dependsOn("runClient") + group = "project" } } - -tasks.register("publishModsSpecificVersions") { - description = "Publish specific versions only, as specified by the PUBLISH_VERSIONS environment variable." - group = "project" - - var versions = providers.environmentVariable("PUBLISH_VERSIONS").map { it.split(",") } - dependsOn(stonecutter.tasks.named("publishMods") { versions.get().contains(metadata.project) }) -} \ No newline at end of file diff --git a/versions/1.21.11/gradle.properties b/versions/1.21.11/gradle.properties new file mode 100644 index 0000000..1dcf381 --- /dev/null +++ b/versions/1.21.11/gradle.properties @@ -0,0 +1,8 @@ +platform.versions=1.21.11 +minecraft.version=1.21.11 +parchment.version=1.21.11:2025.12.20 + +fabric.loaderVersion=0.18.4 +fabric.apiVersion=0.141.2+1.21.11 + +neoforge.version=21.11.37-beta \ No newline at end of file diff --git a/versions/26.1/gradle.properties b/versions/26.1/gradle.properties deleted file mode 100644 index b8215f2..0000000 --- a/versions/26.1/gradle.properties +++ /dev/null @@ -1,8 +0,0 @@ -minecraft.version=26.1.2 -minecraft.dependency=~26.1 - -fabric.loader.version=0.19.2 -fabric.api.version=0.147.0+26.1.2 -neoforge.version=26.1.2.30-beta - -tapestry.version=1.1.1+mc26.1.2 \ No newline at end of file From 144ca1ae87310428a0def7bafd926140e9895b69 Mon Sep 17 00:00:00 2001 From: J4PC Date: Sun, 10 May 2026 16:38:40 +0200 Subject: [PATCH 6/9] Revert "refactor: use pushCommand for consistency and simplify overflow handler" This reverts commit 6765249b105cb6ed61d069a23fe9488fd4f9dba6. --- .../screen/ScribbleBookEditScreen.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java index d94bbfb..e0bb561 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java @@ -170,7 +170,7 @@ protected void initPageButtons(int y) { () -> { PageInsertCommand command = new PageInsertCommand(this.currentPage + pageOffset); command.execute(this); - this.pushCommand(command); + commandManager.push(command); }, getBackgroundX() + 78 + xOffset + i * 126, y + 2, 12, 90, 12, 12))); this.deletePageButtons.add(addRenderableWidget(new IconButtonWidget(deleteText, @@ -178,7 +178,7 @@ protected void initPageButtons(int y) { PageDeleteCommand command = new PageDeleteCommand(this.currentPage + pageOffset, this.pages.get(this.currentPage + pageOffset), 1); // Navigate right command.execute(this); - this.pushCommand(command); + commandManager.push(command); }, getBackgroundX() + 94 + xOffset + i * 126, y + 2, 0, 90, 12, 12))); } @@ -215,7 +215,11 @@ protected void setInitialFocus() { protected TextArea createTextArea(int x, int y, int width, int height, int pageOffset) { // Create the overflow handler lazily (needs font) if (this.overflowHandler == null) { - this.overflowHandler = new TextOverflowHandler(this, this.font, this::pushCommand); + this.overflowHandler = new TextOverflowHandler(this, this.font, (cmd) -> { + this.commandManager.push(cmd); + this.dirty = true; + this.invalidateActionButtons(); + }); } // Create an overflow handler for this specific edit box @@ -263,7 +267,7 @@ public boolean handleEnterAtEnd() { int page = currentPage + pageOffset; PageInsertCommand command = new PageInsertCommand(page + 1); command.execute(ScribbleBookEditScreen.this); - pushCommand(command); + commandManager.push(command); return true; } @@ -274,7 +278,7 @@ public boolean handleBackspaceOnEmpty() { if (page <= 0) return false; PageDeleteCommand command = new PageDeleteCommand(page, pages.get(page), -1); // Navigate left command.execute(ScribbleBookEditScreen.this); - pushCommand(command); + commandManager.push(command); return true; } }; @@ -516,15 +520,10 @@ private boolean isEmpty() { //endregion //region History - public void pushCommand(Command command) { - this.pushCommand(0, command); - } - public void pushCommand(int pageOffset, Command command) { if (command instanceof EditCommand editCommand) { editCommand.page = this.currentPage + pageOffset; } - // OverflowCommand, PageInsertCommand, PageDeleteCommand have their state set at construction time this.commandManager.push(command); this.dirty = true; From ed85c28d193604fdcbbabdad2b3e808cce0d8f09 Mon Sep 17 00:00:00 2001 From: J4PC Date: Sun, 10 May 2026 16:44:42 +0200 Subject: [PATCH 7/9] Reapply "refactor: use pushCommand for consistency and simplify overflow handler" This reverts commit 144ca1ae87310428a0def7bafd926140e9895b69. --- .../screen/ScribbleBookEditScreen.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java index e0bb561..d94bbfb 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java @@ -170,7 +170,7 @@ protected void initPageButtons(int y) { () -> { PageInsertCommand command = new PageInsertCommand(this.currentPage + pageOffset); command.execute(this); - commandManager.push(command); + this.pushCommand(command); }, getBackgroundX() + 78 + xOffset + i * 126, y + 2, 12, 90, 12, 12))); this.deletePageButtons.add(addRenderableWidget(new IconButtonWidget(deleteText, @@ -178,7 +178,7 @@ protected void initPageButtons(int y) { PageDeleteCommand command = new PageDeleteCommand(this.currentPage + pageOffset, this.pages.get(this.currentPage + pageOffset), 1); // Navigate right command.execute(this); - commandManager.push(command); + this.pushCommand(command); }, getBackgroundX() + 94 + xOffset + i * 126, y + 2, 0, 90, 12, 12))); } @@ -215,11 +215,7 @@ protected void setInitialFocus() { protected TextArea createTextArea(int x, int y, int width, int height, int pageOffset) { // Create the overflow handler lazily (needs font) if (this.overflowHandler == null) { - this.overflowHandler = new TextOverflowHandler(this, this.font, (cmd) -> { - this.commandManager.push(cmd); - this.dirty = true; - this.invalidateActionButtons(); - }); + this.overflowHandler = new TextOverflowHandler(this, this.font, this::pushCommand); } // Create an overflow handler for this specific edit box @@ -267,7 +263,7 @@ public boolean handleEnterAtEnd() { int page = currentPage + pageOffset; PageInsertCommand command = new PageInsertCommand(page + 1); command.execute(ScribbleBookEditScreen.this); - commandManager.push(command); + pushCommand(command); return true; } @@ -278,7 +274,7 @@ public boolean handleBackspaceOnEmpty() { if (page <= 0) return false; PageDeleteCommand command = new PageDeleteCommand(page, pages.get(page), -1); // Navigate left command.execute(ScribbleBookEditScreen.this); - commandManager.push(command); + pushCommand(command); return true; } }; @@ -520,10 +516,15 @@ private boolean isEmpty() { //endregion //region History + public void pushCommand(Command command) { + this.pushCommand(0, command); + } + public void pushCommand(int pageOffset, Command command) { if (command instanceof EditCommand editCommand) { editCommand.page = this.currentPage + pageOffset; } + // OverflowCommand, PageInsertCommand, PageDeleteCommand have their state set at construction time this.commandManager.push(command); this.dirty = true; From 9a97098ff6045d6d1e1b7b87e3ef3ed64481600e Mon Sep 17 00:00:00 2001 From: J4PC Date: Sun, 10 May 2026 16:44:42 +0200 Subject: [PATCH 8/9] Reapply "Merge upstream/main (26.1.2)" This reverts commit 18f88532acce333855b1671967b09902be386439. --- .github/workflows/build.yml | 21 +- .github/workflows/release.yml | 59 +++-- .github/workflows/release_specific.yml | 49 ----- .gitignore | 3 + CHANGELOG.md | 20 ++ README.md | 10 +- build.gradle.kts | 90 ++++---- common/build.gradle.kts | 12 + .../java/me/chrr/scribble/ExceptionUtil.java | 43 ++++ .../java/me/chrr/scribble/KeyboardUtil.java | 0 .../main/java/me/chrr/scribble/Scribble.java | 38 ++++ .../java/me/chrr/scribble/ScribbleConfig.java | 6 +- .../me/chrr/scribble/SetReturnScreen.java | 0 .../java/me/chrr/scribble/book/BookFile.java | 4 +- .../me/chrr/scribble/book/FileChooser.java | 4 +- .../java/me/chrr/scribble/book/RichText.java | 0 .../scribble/book/TextOverflowHandler.java | 0 .../me/chrr/scribble/gui/BookTextWidget.java | 8 +- .../chrr/scribble/gui/PageNumberWidget.java | 15 +- .../java/me/chrr/scribble/gui/TextArea.java | 0 .../gui/button/ColorSwatchWidget.java | 4 +- .../scribble/gui/button/IconButtonWidget.java | 4 +- .../gui/button/ModifierButtonWidget.java | 4 +- .../scribble/gui/edit/OverflowHandler.java | 0 .../chrr/scribble/gui/edit/RichEditBox.java | 16 +- .../gui/edit/RichMultiLineTextField.java | 12 +- .../chrr/scribble/history/CommandManager.java | 2 +- .../scribble/history/HistoryListener.java | 0 .../scribble/history/command/Command.java | 0 .../scribble/history/command/EditCommand.java | 0 .../history/command/OverflowCommand.java | 0 .../history/command/PageDeleteCommand.java | 0 .../history/command/PageInsertCommand.java | 0 .../scribble/mixin/BookSignScreenMixin.java | 16 +- .../mixin/ClientPacketListenerMixin.java | 2 +- .../scribble/mixin/KeyboardHandlerMixin.java | 0 .../chrr/scribble/mixin/LocalPlayerMixin.java | 2 +- .../chrr/scribble/mixin/MenuScreensMixin.java | 2 +- .../screen/ScribbleBookEditScreen.java | 35 ++- .../scribble/screen/ScribbleBookScreen.java | 20 +- .../screen/ScribbleBookViewScreen.java | 12 +- .../screen/ScribbleLecternScreen.java | 0 .../main/resources/assets/scribble/banner.png | Bin .../main/resources/assets/scribble/icon.png | Bin .../resources/assets/scribble/lang/de_de.json | 0 .../resources/assets/scribble/lang/en_us.json | 5 +- .../resources/assets/scribble/lang/es_mx.json | 0 .../resources/assets/scribble/lang/ru_ru.json | 0 .../resources/assets/scribble/lang/tr_tr.json | 0 .../resources/assets/scribble/lang/zh_cn.json | 0 .../resources/assets/scribble/lang/zh_tw.json | 0 .../assets/scribble/textures/gui/book_2.png | Bin .../textures/gui/scribble_widgets.png | Bin .../main/resources/scribble.accesswidener | 2 +- .../src}/main/resources/scribble.mixins.json | 0 fabric/build.gradle.kts | 135 +----------- fabric/gradle.properties | 2 - .../chrr/scribble/fabric/ModMenuCompat.java | 2 + .../chrr/scribble/fabric/ScribbleFabric.java | 25 +-- fabric/src/main/resources/fabric.mod.json | 41 ---- gradle.properties | 24 +- gradle/wrapper/gradle-wrapper.properties | 2 +- neoforge/build.gradle.kts | 131 ----------- neoforge/gradle.properties | 2 - .../scribble/neoforge/ScribbleNeoForge.java | 25 +-- .../resources/META-INF/neoforge.mods.toml | 21 -- neoforge/src/main/resources/pack.mcmeta | 6 - settings.gradle.kts | 13 +- src/main/java/me/chrr/scribble/Scribble.java | 62 ------ .../java/me/chrr/tapestry/config/Config.java | 16 -- .../tapestry/config/ConfigEnvironment.java | 7 - .../me/chrr/tapestry/config/ConfigIo.java | 108 --------- .../chrr/tapestry/config/NamingStrategy.java | 40 ---- .../java/me/chrr/tapestry/config/Option.java | 20 -- .../chrr/tapestry/config/ReflectedConfig.java | 206 ------------------ .../tapestry/config/annotation/Header.java | 15 -- .../tapestry/config/annotation/Hidden.java | 14 -- .../config/annotation/SerializeName.java | 22 -- .../config/annotation/TranslationPrefix.java | 15 -- .../config/annotation/UpgradeRewriter.java | 15 -- .../chrr/tapestry/config/gui/IconButton.java | 52 ----- .../chrr/tapestry/config/gui/OptionList.java | 142 ------------ .../chrr/tapestry/config/gui/OptionProxy.java | 27 --- .../config/gui/TapestryConfigScreen.java | 140 ------------ .../gui/widget/BooleanOptionWidget.java | 40 ---- .../config/gui/widget/EnumOptionWidget.java | 36 --- .../config/gui/widget/OptionWidget.java | 105 --------- .../gui/widget/ReadOnlyOptionWidget.java | 20 -- .../config/gui/widget/SliderOptionWidget.java | 160 -------------- .../tapestry/config/value/Constraint.java | 15 -- .../tapestry/config/value/TrackedValue.java | 36 --- .../me/chrr/tapestry/config/value/Value.java | 38 ---- .../tapestry/config/value/VirtualValue.java | 41 ---- .../tapestry-config/textures/gui/reset.png | Bin 156 -> 0 bytes stonecutter.gradle.kts | 19 +- versions/1.21.11/gradle.properties | 8 - versions/26.1/gradle.properties | 8 + 97 files changed, 380 insertions(+), 1996 deletions(-) delete mode 100644 .github/workflows/release_specific.yml create mode 100644 common/build.gradle.kts create mode 100644 common/src/main/java/me/chrr/scribble/ExceptionUtil.java rename {src => common/src}/main/java/me/chrr/scribble/KeyboardUtil.java (100%) create mode 100644 common/src/main/java/me/chrr/scribble/Scribble.java rename {src => common/src}/main/java/me/chrr/scribble/ScribbleConfig.java (95%) rename {src => common/src}/main/java/me/chrr/scribble/SetReturnScreen.java (100%) rename {src => common/src}/main/java/me/chrr/scribble/book/BookFile.java (94%) rename {src => common/src}/main/java/me/chrr/scribble/book/FileChooser.java (98%) rename {src => common/src}/main/java/me/chrr/scribble/book/RichText.java (100%) rename {src => common/src}/main/java/me/chrr/scribble/book/TextOverflowHandler.java (100%) rename {src => common/src}/main/java/me/chrr/scribble/gui/BookTextWidget.java (92%) rename {src => common/src}/main/java/me/chrr/scribble/gui/PageNumberWidget.java (90%) rename {src => common/src}/main/java/me/chrr/scribble/gui/TextArea.java (100%) rename {src => common/src}/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java (92%) rename {src => common/src}/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java (91%) rename {src => common/src}/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java (93%) rename {src => common/src}/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java (100%) rename {src => common/src}/main/java/me/chrr/scribble/gui/edit/RichEditBox.java (95%) rename {src => common/src}/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java (96%) rename {src => common/src}/main/java/me/chrr/scribble/history/CommandManager.java (94%) rename {src => common/src}/main/java/me/chrr/scribble/history/HistoryListener.java (100%) rename {src => common/src}/main/java/me/chrr/scribble/history/command/Command.java (100%) rename {src => common/src}/main/java/me/chrr/scribble/history/command/EditCommand.java (100%) rename {src => common/src}/main/java/me/chrr/scribble/history/command/OverflowCommand.java (100%) rename {src => common/src}/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java (100%) rename {src => common/src}/main/java/me/chrr/scribble/history/command/PageInsertCommand.java (100%) rename {src => common/src}/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java (76%) rename {src => common/src}/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java (93%) rename {src => common/src}/main/java/me/chrr/scribble/mixin/KeyboardHandlerMixin.java (100%) rename {src => common/src}/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java (94%) rename {src => common/src}/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java (93%) rename {src => common/src}/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java (93%) rename {src => common/src}/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java (90%) rename {src => common/src}/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java (84%) rename {src => common/src}/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java (100%) rename {src => common/src}/main/resources/assets/scribble/banner.png (100%) rename {src => common/src}/main/resources/assets/scribble/icon.png (100%) rename {src => common/src}/main/resources/assets/scribble/lang/de_de.json (100%) rename {src => common/src}/main/resources/assets/scribble/lang/en_us.json (89%) rename {src => common/src}/main/resources/assets/scribble/lang/es_mx.json (100%) rename {src => common/src}/main/resources/assets/scribble/lang/ru_ru.json (100%) rename {src => common/src}/main/resources/assets/scribble/lang/tr_tr.json (100%) rename {src => common/src}/main/resources/assets/scribble/lang/zh_cn.json (100%) rename {src => common/src}/main/resources/assets/scribble/lang/zh_tw.json (100%) rename {src => common/src}/main/resources/assets/scribble/textures/gui/book_2.png (100%) rename {src => common/src}/main/resources/assets/scribble/textures/gui/scribble_widgets.png (100%) rename {src => common/src}/main/resources/scribble.accesswidener (99%) rename {src => common/src}/main/resources/scribble.mixins.json (100%) delete mode 100644 fabric/gradle.properties delete mode 100644 fabric/src/main/resources/fabric.mod.json delete mode 100644 neoforge/gradle.properties delete mode 100644 neoforge/src/main/resources/META-INF/neoforge.mods.toml delete mode 100644 neoforge/src/main/resources/pack.mcmeta delete mode 100644 src/main/java/me/chrr/scribble/Scribble.java delete mode 100644 src/main/java/me/chrr/tapestry/config/Config.java delete mode 100644 src/main/java/me/chrr/tapestry/config/ConfigEnvironment.java delete mode 100644 src/main/java/me/chrr/tapestry/config/ConfigIo.java delete mode 100644 src/main/java/me/chrr/tapestry/config/NamingStrategy.java delete mode 100644 src/main/java/me/chrr/tapestry/config/Option.java delete mode 100644 src/main/java/me/chrr/tapestry/config/ReflectedConfig.java delete mode 100644 src/main/java/me/chrr/tapestry/config/annotation/Header.java delete mode 100644 src/main/java/me/chrr/tapestry/config/annotation/Hidden.java delete mode 100644 src/main/java/me/chrr/tapestry/config/annotation/SerializeName.java delete mode 100644 src/main/java/me/chrr/tapestry/config/annotation/TranslationPrefix.java delete mode 100644 src/main/java/me/chrr/tapestry/config/annotation/UpgradeRewriter.java delete mode 100644 src/main/java/me/chrr/tapestry/config/gui/IconButton.java delete mode 100644 src/main/java/me/chrr/tapestry/config/gui/OptionList.java delete mode 100644 src/main/java/me/chrr/tapestry/config/gui/OptionProxy.java delete mode 100644 src/main/java/me/chrr/tapestry/config/gui/TapestryConfigScreen.java delete mode 100644 src/main/java/me/chrr/tapestry/config/gui/widget/BooleanOptionWidget.java delete mode 100644 src/main/java/me/chrr/tapestry/config/gui/widget/EnumOptionWidget.java delete mode 100644 src/main/java/me/chrr/tapestry/config/gui/widget/OptionWidget.java delete mode 100644 src/main/java/me/chrr/tapestry/config/gui/widget/ReadOnlyOptionWidget.java delete mode 100644 src/main/java/me/chrr/tapestry/config/gui/widget/SliderOptionWidget.java delete mode 100644 src/main/java/me/chrr/tapestry/config/value/Constraint.java delete mode 100644 src/main/java/me/chrr/tapestry/config/value/TrackedValue.java delete mode 100644 src/main/java/me/chrr/tapestry/config/value/Value.java delete mode 100644 src/main/java/me/chrr/tapestry/config/value/VirtualValue.java delete mode 100644 src/main/resources/assets/tapestry-config/textures/gui/reset.png delete mode 100644 versions/1.21.11/gradle.properties create mode 100644 versions/26.1/gradle.properties diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4251531..e9193b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,13 +14,24 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - - name: Set up JDK 21 + - name: Setup JDK 25 uses: actions/setup-java@v4 with: - java-version: '21' + java-version: '25' distribution: 'temurin' - - name: Run Build - run: ./gradlew buildAndCollect + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build JARs + run: ./gradlew build + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: mod-snapshots + path: versions/*/build/libs/scribble-*.jar + retention-days: 7 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e03edc1..e4ec7fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,16 @@ -name: Publish +name: Release on: workflow_dispatch: + inputs: + version: + description: 'Version number' + required: true + type: string + minecraftVersions: + description: 'Minecraft versions (optional, comma-separated)' + required: false + type: string jobs: release: @@ -11,33 +20,49 @@ jobs: contents: write steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - - name: Set up JDK 21 + - name: Setup JDK 25 uses: actions/setup-java@v4 with: - java-version: '21' + java-version: '25' distribution: 'temurin' - - name: Run Build - run: ./gradlew buildAndCollect + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 - - name: Fetch changelog + - name: Update version in gradle.properties + id: set-version run: | - { - echo "CHANGELOG<> $GITHUB_ENV + sed -i "s/^mod\.version=.*/mod\.version=${{ github.event.inputs.version }}/" gradle.properties - - run: ./gradlew publishMods + - name: Publish to Modrinth and CurseForge + if: ${{ !github.event.inputs.versions }} + run: ./gradlew publishMods env: - CHANGELOG: ${{ env.CHANGELOG }} MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} + RELEASE: 'true' - - name: Create release + - name: Publish specified versions to Modrinth and CurseForge + if: ${{ github.event.inputs.versions }} + run: ./gradlew publishModsSpecificVersions + env: + PUBLISH_VERSIONS: ${{ github.event.inputs.versions }} + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} + RELEASE: 'true' + + - name: Commit and push version change + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: 'chore: release ${{ github.event.inputs.version }}' + tag_name: v${{ github.event.inputs.version }} + + - name: Create GitHub release uses: softprops/action-gh-release@v2 with: - body: ${{ env.CHANGELOG }} - files: build/libs/*.jar \ No newline at end of file + tag_name: v${{ github.event.inputs.version }} + body: 'Read more on [Modrinth](https://modrinth.com/mod/scribble).' + files: versions/*/build/libs/scribble-*.jar \ No newline at end of file diff --git a/.github/workflows/release_specific.yml b/.github/workflows/release_specific.yml deleted file mode 100644 index 2a83484..0000000 --- a/.github/workflows/release_specific.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Publish Specific - -on: - workflow_dispatch: - inputs: - version: - description: 'Minecraft version' - required: true - type: string - - -jobs: - release: - runs-on: ubuntu-latest - - permissions: - contents: write - - steps: - - uses: actions/checkout@v4 - - - name: Set up JDK 21 - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - - - name: Run Build - run: ./gradlew buildAndCollect - - - name: Fetch changelog - run: | - { - echo "CHANGELOG<> $GITHUB_ENV - - - run: ./gradlew :fabric:${{ inputs.version }}:publishMods :neoforge:${{ inputs.version }}:publishMods - env: - CHANGELOG: ${{ env.CHANGELOG }} - MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} - CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} - - - name: Create release - uses: softprops/action-gh-release@v2 - with: - body: ${{ env.CHANGELOG }} - files: build/libs/*.jar \ No newline at end of file diff --git a/.gitignore b/.gitignore index 83e6380..4bf7829 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,8 @@ build/ run/ logs/ +versions/ +!/versions + .idea/ .vscode/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 661a1c6..601958c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## 2.0.2 + +- Show error dialogs when saving and loading books fails. +- Don't reset current formatting when cursor updates, but doesn't move. +- Mark versions compatible with all past and future drop versions (e.g. `~26.1` becomes `>=26.1 <26.2`). + +## 2.0.1 + +- Update Tapestry, which fixes some config things. + +## 2.0.0 + +Scribble's been upgraded and is out of beta! Thanks to Mojang deobfuscating the game, you can now use the same JAR file +for both Fabric and NeoForge! There's lots of other changes too if you're upgrading from a 1.x version, so see the +previous changelog entries. + +- Update to 26.1, drop support for older versions. +- Use Tapestry Gradle to build the mod (and a merged JAR!) instead of Architectury. +- Always encode exported book files using UTF-8. + ## 2.0.0-beta2 - Move from YACL to a custom config system. diff --git a/README.md b/README.md index e214ecf..7c829c7 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,12 @@ or [CurseForge](https://curseforge.com/minecraft/mc-mods/scribble). Scribble supports multiple Minecraft versions using [Stonecutter](https://stonecutter.kikugie.dev/). The easiest way to interact with this is by using an IDE such as IntelliJ. To switch between versions, -use the Gradle tasks under the `stonecutter` category. To run the game, use the `runActive{Fabric,Forge,Neoforge}` +use the Gradle tasks under the `stonecutter` category. To run the game, use the `runActive{Fabric,Neoforge}` tasks. -Make sure to switch back to `1.21.4` to commit changes. +Make sure to switch back to the latest version to commit changes. ### Release checklist -- Update the version number. - - Change in `gradle.properties`. - - Add an entry in `CHANGELOG.md`. -- Commit and push a new tag. (example: `v1.2.3`) - - Tag name is the version number prefixed by `v`. +- Add a changelog entry to `CHANGELOG.md`. - Manually trigger the Publish workflow on GitHub. diff --git a/build.gradle.kts b/build.gradle.kts index 1ba8df6..0fe4d1b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,52 +1,66 @@ +import me.chrr.tapestry.gradle.Environment + plugins { - id("dev.architectury.loom") - id("architectury-plugin") + id("me.chrr.tapestry.gradle") } -fun Project.hasProp(namespace: String, key: String) = hasProperty("$namespace.$key") -fun Project.prop(namespace: String, key: String) = property("$namespace.$key") as String - +val root = stonecutter.current.project val current = stonecutter.current.version -val minecraft = prop("minecraft", "version") -group = prop("mod", "group") -version = "${prop("mod", "version")}+mc$current-common" -base.archivesName.set(prop("mod", "name")) +fun sibling(name: String) = + stonecutter.node.sibling(name)!!.project -architectury.injectInjectables = false -architectury.common(stonecutter.tree.branches.mapNotNull { - if (stonecutter.current.project !in it) null - else if (!it.project.hasProp("loom", "platform")) null - else it.project.prop("loom", "platform") -}) +tapestry { + projects { + common = listOf(sibling("common")) + fabric = listOf(sibling("fabric")) + neoforge = listOf(sibling("neoforge")) + } -repositories { - maven("https://maven.parchmentmc.org") -} + versions { + minecraft = prop("minecraft.version") + fabricLoader = prop("fabric.loader.version") + neoforge = prop("neoforge.version") + } -dependencies { - minecraft("com.mojang:minecraft:$minecraft") + info { + id = "scribble" + version = prop("mod.version") - @Suppress("UnstableApiUsage") - mappings(loom.layered { - officialMojangMappings() - parchment("org.parchmentmc.data:parchment-${prop("parchment", "version")}@zip") - }) + name = "Scribble" + description = "Expertly edit your books with rich formatting options, page utilities and more!" + authors = listOf("chrrrs") + license = "MIT" + environment = Environment.Client - modImplementation("net.fabricmc:fabric-loader:${prop("fabric", "loaderVersion")}") -} + url = "https://github.com/chrrs/scribble" + sources = "https://github.com/chrrs/scribble" + issues = "https://github.com/chrrs/scribble/issues" -loom { - accessWidenerPath = rootProject.file("src/main/resources/scribble.accesswidener") -} + icon = "assets/scribble/icon.png" + banner = "assets/scribble/banner.png" + } -java { - targetCompatibility = JavaVersion.VERSION_21 - sourceCompatibility = JavaVersion.VERSION_21 -} + transform { + classTweaker = "scribble.accesswidener" + mixinConfigs.add("scribble.mixins.json") + } -tasks { - jar { - from("LICENSE") + depends { + minecraft.version(prop("minecraft.dependency").get()) + mod(fabric = "fabric-resource-loader-v1") + mod("tapestry_config") + mod("tapestry") } -} + + game { + runDir = rootProject.file("run") + username = "chrrz" + } + + publish { + readChangelogFrom(rootProject.file("CHANGELOG.md")) + modrinth = "yXAvIk0x" + curseforge = "1051344" + } +} \ No newline at end of file diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 0000000..2ed33df --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,12 @@ +repositories { + maven("https://maven.chrr.me/releases") +} + +dependencies { + val root = stonecutter.node.sibling("")!!.project + fun tapestryModule(name: String) = + implementation("me.chrr.tapestry:$name:${root.property("tapestry.version")}") + + jij(tapestryModule("tapestry-base")!!) + jij(tapestryModule("tapestry-config")!!) +} \ No newline at end of file diff --git a/common/src/main/java/me/chrr/scribble/ExceptionUtil.java b/common/src/main/java/me/chrr/scribble/ExceptionUtil.java new file mode 100644 index 0000000..faa63a6 --- /dev/null +++ b/common/src/main/java/me/chrr/scribble/ExceptionUtil.java @@ -0,0 +1,43 @@ +package me.chrr.scribble; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import org.jspecify.annotations.NullMarked; + +import java.util.Arrays; +import java.util.Optional; + +@NullMarked +public class ExceptionUtil { + private ExceptionUtil() { + } + + public static Component getExceptionSummary(Throwable throwable) { + MutableComponent out = Component.empty(); + + boolean first = true; + Throwable cause = throwable; + while (cause != null) { + if (first) { + first = false; + } else { + out.append("\n\n"); + out.append(Component.literal("caused by: ").withStyle(ChatFormatting.GRAY)); + } + + String header = cause.getClass().getName() + ": " + cause.getMessage(); + out.append(Component.literal(header).withStyle(ChatFormatting.GRAY)); + + Optional source = Arrays.stream(cause.getStackTrace()) + .map(StackTraceElement::toString) + .filter(s -> s.contains("me.chrr.")) + .findFirst(); + source.ifPresent(it -> out.append(Component.literal("\nat " + it).withStyle(ChatFormatting.DARK_GRAY))); + + cause = cause.getCause(); + } + + return out; + } +} diff --git a/src/main/java/me/chrr/scribble/KeyboardUtil.java b/common/src/main/java/me/chrr/scribble/KeyboardUtil.java similarity index 100% rename from src/main/java/me/chrr/scribble/KeyboardUtil.java rename to common/src/main/java/me/chrr/scribble/KeyboardUtil.java diff --git a/common/src/main/java/me/chrr/scribble/Scribble.java b/common/src/main/java/me/chrr/scribble/Scribble.java new file mode 100644 index 0000000..2a111e8 --- /dev/null +++ b/common/src/main/java/me/chrr/scribble/Scribble.java @@ -0,0 +1,38 @@ +package me.chrr.scribble; + +import me.chrr.scribble.book.FileChooser; +import me.chrr.tapestry.base.Tapestry; +import me.chrr.tapestry.config.ReflectedConfig; +import me.chrr.tapestry.config.gui.TapestryConfigScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.resources.Identifier; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jspecify.annotations.NullMarked; + +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; + +@NullMarked +public class Scribble { + public static final String MOD_ID = "scribble"; + public static final String VERSION = Objects.requireNonNull(Tapestry.PLATFORM_METHODS.getModVersion(MOD_ID)); + + public static final Logger LOGGER = LogManager.getLogger(); + + public static final Path BOOK_DIR = Tapestry.PLATFORM_METHODS.getGameDirectory().resolve("books"); + public static final ScribbleConfig CONFIG = ReflectedConfig.load(ScribbleConfig.class, "scribble.client.json", List.of("scribble.json")); + + public static void init() { + FileChooser.convertLegacyBooks(); + } + + public static Screen buildConfigScreen(Screen parent) { + return new TapestryConfigScreen(parent, CONFIG); + } + + public static Identifier id(String path) { + return Identifier.fromNamespaceAndPath(MOD_ID, path); + } +} diff --git a/src/main/java/me/chrr/scribble/ScribbleConfig.java b/common/src/main/java/me/chrr/scribble/ScribbleConfig.java similarity index 95% rename from src/main/java/me/chrr/scribble/ScribbleConfig.java rename to common/src/main/java/me/chrr/scribble/ScribbleConfig.java index 7feb8f8..8d77414 100644 --- a/src/main/java/me/chrr/scribble/ScribbleConfig.java +++ b/common/src/main/java/me/chrr/scribble/ScribbleConfig.java @@ -15,20 +15,20 @@ public class ScribbleConfig extends ReflectedConfig { @Hidden public Value pagesToShow = value(1); - @Header("appearance") + @Category("appearance") public Value doublePageViewing = map(pagesToShow, (n) -> n > 1, (b) -> b ? 2 : 1); public Value centerBookGui = value(true); public Value showFormattingButtons = value(true); public Value showActionButtons = value(ShowActionButtons.WHEN_EDITING); - @Header("behaviour") + @Category("behaviour") public Value copyFormattingCodes = value(true); public Value editHistorySize = value(32) .range(8, 128, 1); public Value overflowWhenTyping = value(false); public Value pasteBehavior = value(PasteBehavior.DENY); - @Header("miscellaneous") + @Category("miscellaneous") public Value openVanillaBookScreenOnShift = value(false); @UpgradeRewriter(currentVersion = 3) diff --git a/src/main/java/me/chrr/scribble/SetReturnScreen.java b/common/src/main/java/me/chrr/scribble/SetReturnScreen.java similarity index 100% rename from src/main/java/me/chrr/scribble/SetReturnScreen.java rename to common/src/main/java/me/chrr/scribble/SetReturnScreen.java diff --git a/src/main/java/me/chrr/scribble/book/BookFile.java b/common/src/main/java/me/chrr/scribble/book/BookFile.java similarity index 94% rename from src/main/java/me/chrr/scribble/book/BookFile.java rename to common/src/main/java/me/chrr/scribble/book/BookFile.java index 5873a33..c30b66f 100644 --- a/src/main/java/me/chrr/scribble/book/BookFile.java +++ b/common/src/main/java/me/chrr/scribble/book/BookFile.java @@ -34,7 +34,7 @@ public static BookFile readFile(Path path) throws IOException { } else if (path.toString().endsWith(".json")) { return readJson(path); } else { - throw new IOException("unrecognized file format"); + throw new IOException("Unrecognized file format."); } } @@ -51,7 +51,7 @@ private static BookFile readNbt(Path path) throws IOException { CompoundTag root = NbtIo.read(path); if (root == null) { - throw new IOException("could not read book nbt file"); + throw new IOException("Could not read book NBT file."); } String author = root.getString("author").orElse(""); diff --git a/src/main/java/me/chrr/scribble/book/FileChooser.java b/common/src/main/java/me/chrr/scribble/book/FileChooser.java similarity index 98% rename from src/main/java/me/chrr/scribble/book/FileChooser.java rename to common/src/main/java/me/chrr/scribble/book/FileChooser.java index a628e7a..8b9dd26 100644 --- a/src/main/java/me/chrr/scribble/book/FileChooser.java +++ b/common/src/main/java/me/chrr/scribble/book/FileChooser.java @@ -81,7 +81,7 @@ public static void chooseFile(boolean save, Consumer pathConsumer) { * @return the book directory. */ private static Path createAndGetBookDirectory() { - Path bookDir = Scribble.platform().BOOK_DIR; + Path bookDir = Scribble.BOOK_DIR; try { if (!Files.exists(bookDir)) { @@ -99,7 +99,7 @@ private static Path createAndGetBookDirectory() { * new JSON format. All old files are copied to the '_legacy' directory. */ public static void convertLegacyBooks() { - Path rootDir = Scribble.platform().BOOK_DIR; + Path rootDir = Scribble.BOOK_DIR; Path legacyDir = rootDir.resolve("_legacy"); if (!rootDir.toFile().isDirectory()) { diff --git a/src/main/java/me/chrr/scribble/book/RichText.java b/common/src/main/java/me/chrr/scribble/book/RichText.java similarity index 100% rename from src/main/java/me/chrr/scribble/book/RichText.java rename to common/src/main/java/me/chrr/scribble/book/RichText.java diff --git a/src/main/java/me/chrr/scribble/book/TextOverflowHandler.java b/common/src/main/java/me/chrr/scribble/book/TextOverflowHandler.java similarity index 100% rename from src/main/java/me/chrr/scribble/book/TextOverflowHandler.java rename to common/src/main/java/me/chrr/scribble/book/TextOverflowHandler.java diff --git a/src/main/java/me/chrr/scribble/gui/BookTextWidget.java b/common/src/main/java/me/chrr/scribble/gui/BookTextWidget.java similarity index 92% rename from src/main/java/me/chrr/scribble/gui/BookTextWidget.java rename to common/src/main/java/me/chrr/scribble/gui/BookTextWidget.java index 34ef5dd..80d543f 100644 --- a/src/main/java/me/chrr/scribble/gui/BookTextWidget.java +++ b/common/src/main/java/me/chrr/scribble/gui/BookTextWidget.java @@ -2,7 +2,7 @@ import net.minecraft.client.gui.ActiveTextCollector; import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.narration.NarratedElementType; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.navigation.ScreenRectangle; @@ -46,14 +46,14 @@ public BookTextWidget(int x, int y, int width, int height, Font font, Consumer onPageChange, int x, int y, Font font) this.afterCursor = Component.empty(); } - public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float deltaTicks) { + @Override + protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { if (this.isFocused()) { int x = this.getX() + this.width; x -= this.font.width(this.afterCursor); - graphics.drawString(this.font, this.afterCursor, x, this.getY(), CommonColors.BLACK, false); + graphics.text(this.font, this.afterCursor, x, this.getY(), CommonColors.BLACK, false); int cursorX = x; x -= this.font.width(this.input); - graphics.drawString(this.font, this.input, x, this.getY(), CommonColors.BLACK, false); + graphics.text(this.font, this.input, x, this.getY(), CommonColors.BLACK, false); x -= this.font.width(this.beforeCursor); - graphics.drawString(this.font, this.beforeCursor, x, this.getY(), CommonColors.BLACK, false); + graphics.text(this.font, this.beforeCursor, x, this.getY(), CommonColors.BLACK, false); if ((Util.getMillis() - this.lastSwitchFocusTime) / 300L % 2L == 0L) { graphics.fill(cursorX, this.getY() - 1, cursorX + 1, this.getY() + 1 + font.lineHeight, CommonColors.BLACK); @@ -77,7 +78,7 @@ public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float del Component text = this.isHovered() ? this.hoverText : this.text; int textWidth = font.width(text); - graphics.drawString(this.font, text, this.getX() + this.width - textWidth, this.getY(), color, false); + graphics.text(this.font, text, this.getX() + this.width - textWidth, this.getY(), color, false); } if (this.isHovered()) { @@ -92,8 +93,6 @@ protected void updateWidgetNarration(NarrationElementOutput narrationElementOutp @Override public boolean charTyped(CharacterEvent event) { - if (event.modifiers() != 0) - return false; if (event.codepoint() < '0' || event.codepoint() > '9') return false; if (this.input.length() >= 2 && !(this.input.equals("10") && event.codepoint() == '0')) diff --git a/src/main/java/me/chrr/scribble/gui/TextArea.java b/common/src/main/java/me/chrr/scribble/gui/TextArea.java similarity index 100% rename from src/main/java/me/chrr/scribble/gui/TextArea.java rename to common/src/main/java/me/chrr/scribble/gui/TextArea.java diff --git a/src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java b/common/src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java similarity index 92% rename from src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java rename to common/src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java index db36d0b..98dab33 100644 --- a/src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java +++ b/common/src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java @@ -1,7 +1,7 @@ package me.chrr.scribble.gui.button; import net.minecraft.ChatFormatting; -import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.AbstractButton; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.narration.NarrationElementOutput; @@ -24,7 +24,7 @@ public ColorSwatchWidget(Component tooltip, ChatFormatting color, Runnable onCli } @Override - protected void renderContents(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + protected void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) { if (isHoveredOrFocused() || toggled) { graphics.fill(getX(), getY(), getX() + width, getY() + height, isHoveredOrFocused() ? 0xffffffff : 0xffa0a0a0); } diff --git a/src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java b/common/src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java similarity index 91% rename from src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java rename to common/src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java index 8abb077..07e35cc 100644 --- a/src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java +++ b/common/src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java @@ -1,7 +1,7 @@ package me.chrr.scribble.gui.button; import me.chrr.scribble.Scribble; -import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.AbstractButton; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.narration.NarrationElementOutput; @@ -30,7 +30,7 @@ public IconButtonWidget(Component tooltip, Runnable onPress, int x, int y, int u } @Override - protected void renderContents(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + protected void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) { int v = this.v + (!this.active ? 2 : (this.isHoveredOrFocused() ? 1 : 0)) * this.height; graphics.blit(RenderPipelines.GUI_TEXTURED, WIDGETS_TEXTURE, getX(), getY(), this.u, v, this.width, this.height, 128, 128); } diff --git a/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java b/common/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java similarity index 93% rename from src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java rename to common/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java index 4287757..330272b 100644 --- a/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java +++ b/common/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java @@ -1,7 +1,7 @@ package me.chrr.scribble.gui.button; import me.chrr.scribble.Scribble; -import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.AbstractButton; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.narration.NarrationElementOutput; @@ -37,7 +37,7 @@ public ModifierButtonWidget(Component tooltip, Consumer onToggle, int x } @Override - protected void renderContents(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + protected void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) { // If the button is hovered or focused, we want it to be in front, so we slightly increase the height. int offset = this.isHoveredOrFocused() ? 1 : 0; diff --git a/src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java b/common/src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java similarity index 100% rename from src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java rename to common/src/main/java/me/chrr/scribble/gui/edit/OverflowHandler.java diff --git a/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java b/common/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java similarity index 95% rename from src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java rename to common/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java index eea2b16..ade3e9f 100644 --- a/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java +++ b/common/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java @@ -11,7 +11,7 @@ import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.MultiLineEditBox; import net.minecraft.client.gui.components.MultilineTextField; import net.minecraft.client.gui.narration.NarratedElementType; @@ -109,12 +109,12 @@ private int getCursorColor() { } @Override - protected void renderContents(GuiGraphics graphics, int mouseX, int mouseY, float deltaTicks) { + protected void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float deltaTicks) { RichText text = getRichTextField().getRichText(); // Draw the placeholder text if there's no content. if (text.isEmpty() && !this.isFocused()) { - graphics.drawWordWrap(this.font, this.placeholder, this.getInnerLeft(), this.getInnerTop(), this.width - this.totalInnerPadding(), 0xcce0e0e0); + graphics.textWithWordWrap(this.font, this.placeholder, this.getInnerLeft(), this.getInnerTop(), this.width - this.totalInnerPadding(), 0xcce0e0e0); return; } @@ -135,7 +135,7 @@ protected void renderContents(GuiGraphics graphics, int mouseX, int mouseY, floa if (visible) { // AD-HOC: Draw the entire line in one call. Vanilla does this differently, I don't know why RichText lineText = text.subText(line.beginIndex(), line.endIndex()); - graphics.drawString(this.font, lineText.getAsMutableComponent(), x, y, this.textColor, this.textShadow); + graphics.text(this.font, lineText.getAsMutableComponent(), x, y, this.textColor, this.textShadow); RichText beforeCursor = text.subText(line.beginIndex(), cursor); lastX = x + this.font.width(beforeCursor); @@ -149,7 +149,7 @@ protected void renderContents(GuiGraphics graphics, int mouseX, int mouseY, floa // Otherwise, just draw the line normally. if (visible) { RichText lineText = text.subText(line.beginIndex(), line.endIndex()); - graphics.drawString(this.font, lineText.getAsMutableComponent(), x, y, this.textColor, this.textShadow); + graphics.text(this.font, lineText.getAsMutableComponent(), x, y, this.textColor, this.textShadow); lastX = x + this.font.width(lineText) - 1; } @@ -162,7 +162,7 @@ protected void renderContents(GuiGraphics graphics, int mouseX, int mouseY, floa // If we haven't drawn the cursor yet, it should be a '_' at the last draw position. if (blink && !cursorInText) { if (this.withinContentAreaTopBottom(lastY, lastY + this.font.lineHeight)) { - graphics.drawString(this.font, "_", lastX + 1, lastY, this.getCursorColor(), this.textShadow); + graphics.text(this.font, "_", lastX + 1, lastY, this.getCursorColor(), this.textShadow); } } @@ -283,7 +283,7 @@ public boolean keyPressed(KeyEvent event) { // Handle Paste with different behaviors based on config if (event.isPaste()) { String clipboardText = Minecraft.getInstance().keyboardHandler.getClipboard().replace("\r", ""); - boolean keepFormatting = Scribble.config().copyFormattingCodes.get() ^ event.hasShiftDown(); + boolean keepFormatting = Scribble.CONFIG.copyFormattingCodes.get() ^ event.hasShiftDown(); if (!keepFormatting) clipboardText = ChatFormatting.stripFormatting(clipboardText); RichText insert = ChatFormatting.stripFormatting(clipboardText).equals(clipboardText) @@ -297,7 +297,7 @@ public boolean keyPressed(KeyEvent event) { .splitLines(result, tf.width, net.minecraft.network.chat.Style.EMPTY).size() > tf.lineLimit; if (wouldOverflow) { - ScribbleConfig.PasteBehavior behavior = Scribble.config().pasteBehavior.get(); + ScribbleConfig.PasteBehavior behavior = Scribble.CONFIG.pasteBehavior.get(); if (behavior == ScribbleConfig.PasteBehavior.FIT_PAGE) { int lo = 0, hi = insert.getLength(), best = 0; while (lo <= hi) { diff --git a/src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java b/common/src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java similarity index 96% rename from src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java rename to common/src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java index 006dd72..9a15a29 100644 --- a/src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java +++ b/common/src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java @@ -108,6 +108,9 @@ public void setValueWithoutUpdating(RichText richText) { } public void resetCursor(boolean update) { + if (this.cursor == this.value.length() && this.selectCursor == this.cursor) + return; + this.cursor = this.value.length(); this.selectCursor = this.cursor; @@ -187,14 +190,19 @@ public void seekCursorToPoint(double x, double y) { @Override public void seekCursor(Whence whence, int amount) { + int cursorBefore = this.cursor; + int selectBefore = this.selectCursor; + super.seekCursor(whence, amount); - this.sendUpdateFormat(); + + if (cursorBefore != this.cursor || selectBefore != this.selectCursor) + this.sendUpdateFormat(); } @Override public boolean keyPressed(KeyEvent event) { // Override copy/cut/paste to remove formatting codes if the config option is set or SHIFT is held down. - boolean keepFormatting = Scribble.config().copyFormattingCodes.get() ^ event.hasShiftDown(); + boolean keepFormatting = Scribble.CONFIG.copyFormattingCodes.get() ^ event.hasShiftDown(); boolean ctrlNoAlt = event.hasControlDown() && !event.hasAltDown(); if (ctrlNoAlt && (KeyboardUtil.isKey(event.key(), "C") || KeyboardUtil.isKey(event.key(), "X"))) { String text = this.getSelectedText(); diff --git a/src/main/java/me/chrr/scribble/history/CommandManager.java b/common/src/main/java/me/chrr/scribble/history/CommandManager.java similarity index 94% rename from src/main/java/me/chrr/scribble/history/CommandManager.java rename to common/src/main/java/me/chrr/scribble/history/CommandManager.java index d5fca7c..84869a4 100644 --- a/src/main/java/me/chrr/scribble/history/CommandManager.java +++ b/common/src/main/java/me/chrr/scribble/history/CommandManager.java @@ -26,7 +26,7 @@ public void push(Command command) { this.commands.add(command); this.index += 1; - if (this.commands.size() > Scribble.config().editHistorySize.get()) { + if (this.commands.size() > Scribble.CONFIG.editHistorySize.get()) { this.commands.removeFirst(); this.index -= 1; } diff --git a/src/main/java/me/chrr/scribble/history/HistoryListener.java b/common/src/main/java/me/chrr/scribble/history/HistoryListener.java similarity index 100% rename from src/main/java/me/chrr/scribble/history/HistoryListener.java rename to common/src/main/java/me/chrr/scribble/history/HistoryListener.java diff --git a/src/main/java/me/chrr/scribble/history/command/Command.java b/common/src/main/java/me/chrr/scribble/history/command/Command.java similarity index 100% rename from src/main/java/me/chrr/scribble/history/command/Command.java rename to common/src/main/java/me/chrr/scribble/history/command/Command.java diff --git a/src/main/java/me/chrr/scribble/history/command/EditCommand.java b/common/src/main/java/me/chrr/scribble/history/command/EditCommand.java similarity index 100% rename from src/main/java/me/chrr/scribble/history/command/EditCommand.java rename to common/src/main/java/me/chrr/scribble/history/command/EditCommand.java diff --git a/src/main/java/me/chrr/scribble/history/command/OverflowCommand.java b/common/src/main/java/me/chrr/scribble/history/command/OverflowCommand.java similarity index 100% rename from src/main/java/me/chrr/scribble/history/command/OverflowCommand.java rename to common/src/main/java/me/chrr/scribble/history/command/OverflowCommand.java diff --git a/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java b/common/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java similarity index 100% rename from src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java rename to common/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java diff --git a/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java b/common/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java similarity index 100% rename from src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java rename to common/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java diff --git a/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java b/common/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java similarity index 76% rename from src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java rename to common/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java index fc651b3..25f0353 100644 --- a/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java +++ b/common/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java @@ -6,7 +6,7 @@ import me.chrr.scribble.SetReturnScreen; import me.chrr.scribble.screen.ScribbleBookScreen; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.Renderable; import net.minecraft.client.gui.components.events.GuiEventListener; @@ -39,7 +39,7 @@ private BookSignScreenMixin(Component title) { this.scribble$returnScreen = screen; } - @WrapOperation(method = "method_71541", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;setScreen(Lnet/minecraft/client/gui/screens/Screen;)V")) + @WrapOperation(method = "lambda$init$2", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;setScreen(Lnet/minecraft/client/gui/screens/Screen;)V")) public void redirectReturnScreen(Minecraft instance, Screen screen, Operation original) { original.call(instance, this.scribble$returnScreen != null ? this.scribble$returnScreen : screen); } @@ -47,7 +47,7 @@ public void redirectReturnScreen(Minecraft instance, Screen screen, Operation T shiftButton // When rendering, we translate the matrices of the draw context to draw the text further down if needed. // Note that this happens after the parent screen render, so only the text in the book is shifted. - @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/Screen;render(Lnet/minecraft/client/gui/GuiGraphics;IIF)V", shift = At.Shift.AFTER)) - public void translateRender(GuiGraphics graphics, int mouseX, int mouseY, float delta, CallbackInfo ci) { + @Inject(method = "extractRenderState", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/Screen;extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", shift = At.Shift.AFTER)) + public void translateRender(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta, CallbackInfo ci) { graphics.pose().pushMatrix(); graphics.pose().translate(0f, scribble$getYOffset()); } // At the end of rendering, we need to pop those matrices we pushed. - @Inject(method = "render", at = @At(value = "RETURN")) - public void popRender(GuiGraphics graphics, int mouseX, int mouseY, float delta, CallbackInfo ci) { + @Inject(method = "extractRenderState", at = @At(value = "RETURN")) + public void popRender(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta, CallbackInfo ci) { graphics.pose().popMatrix(); } @Unique private int scribble$getYOffset() { - if (Scribble.config().centerBookGui.get()) { + if (Scribble.CONFIG.centerBookGui.get()) { // See ScribbleBookScreen#getBackgroundY(). return this.height / 3 - ScribbleBookScreen.getMenuHeight() / 3; } else { diff --git a/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java b/common/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java similarity index 93% rename from src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java rename to common/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java index 0819801..de8b6a8 100644 --- a/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java +++ b/common/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java @@ -18,7 +18,7 @@ public abstract class ClientPacketListenerMixin { @WrapOperation(method = "handleOpenBook", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;setScreen(Lnet/minecraft/client/gui/screens/Screen;)V")) public void overrideBookViewScreen(Minecraft instance, Screen screen, Operation original, @Local BookViewScreen.BookAccess book) { - if (instance.hasShiftDown() && Scribble.config().openVanillaBookScreenOnShift.get()) { + if (instance.hasShiftDown() && Scribble.CONFIG.openVanillaBookScreenOnShift.get()) { original.call(instance, screen); } else { // FIXME: ideally, I'd like to avoid even constructing the original BookViewScreen. diff --git a/src/main/java/me/chrr/scribble/mixin/KeyboardHandlerMixin.java b/common/src/main/java/me/chrr/scribble/mixin/KeyboardHandlerMixin.java similarity index 100% rename from src/main/java/me/chrr/scribble/mixin/KeyboardHandlerMixin.java rename to common/src/main/java/me/chrr/scribble/mixin/KeyboardHandlerMixin.java diff --git a/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java b/common/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java similarity index 94% rename from src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java rename to common/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java index ee79256..d65b7a6 100644 --- a/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java +++ b/common/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java @@ -20,7 +20,7 @@ public abstract class LocalPlayerMixin { @WrapOperation(method = "openItemGui", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;setScreen(Lnet/minecraft/client/gui/screens/Screen;)V")) public void overrideBookViewScreen(Minecraft instance, Screen screen, Operation original, @Local(argsOnly = true) ItemStack itemStack, @Local(argsOnly = true) InteractionHand hand, @Local WritableBookContent book) { - if (instance.hasShiftDown() && Scribble.config().openVanillaBookScreenOnShift.get()) { + if (instance.hasShiftDown() && Scribble.CONFIG.openVanillaBookScreenOnShift.get()) { original.call(instance, screen); } else { // FIXME: ideally, I'd like to avoid even constructing the original BookEditScreen. diff --git a/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java b/common/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java similarity index 93% rename from src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java rename to common/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java index a4b4183..f083fff 100644 --- a/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java +++ b/common/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java @@ -22,7 +22,7 @@ private static > void overrideLectern if (type == MenuType.LECTERN) { original.call(type, (MenuScreens.ScreenConstructor) (menu, inventory, title) -> { Minecraft minecraft = Minecraft.getInstance(); - if (!minecraft.hasShiftDown() || !Scribble.config().openVanillaBookScreenOnShift.get()) { + if (!minecraft.hasShiftDown() || !Scribble.CONFIG.openVanillaBookScreenOnShift.get()) { return (S) new ScribbleLecternScreen(menu); } else { return factory.create(menu, inventory, title); diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java b/common/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java similarity index 93% rename from src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java rename to common/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java index d94bbfb..881de47 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java +++ b/common/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java @@ -1,14 +1,11 @@ package me.chrr.scribble.screen; import it.unimi.dsi.fastutil.booleans.BooleanConsumer; -import me.chrr.scribble.KeyboardUtil; -import me.chrr.scribble.Scribble; -import me.chrr.scribble.SetReturnScreen; +import me.chrr.scribble.*; import me.chrr.scribble.book.BookFile; import me.chrr.scribble.book.FileChooser; import me.chrr.scribble.book.RichText; import me.chrr.scribble.book.TextOverflowHandler; -import me.chrr.scribble.ScribbleConfig; import me.chrr.scribble.gui.TextArea; import me.chrr.scribble.gui.button.ColorSwatchWidget; import me.chrr.scribble.gui.button.IconButtonWidget; @@ -27,6 +24,7 @@ import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.screens.AlertScreen; import net.minecraft.client.gui.screens.ConfirmScreen; import net.minecraft.client.gui.screens.inventory.BookSignScreen; import net.minecraft.client.input.KeyEvent; @@ -107,7 +105,7 @@ public ScribbleBookEditScreen(Player player, ItemStack itemStack, InteractionHan //region Widgets (Action, Menu & TextArea) @Override protected boolean shouldShowActionButtons() { - return Scribble.config().showActionButtons.get() != ScribbleConfig.ShowActionButtons.NEVER; + return Scribble.CONFIG.showActionButtons.get() != ScribbleConfig.ShowActionButtons.NEVER; } @Override @@ -155,8 +153,8 @@ protected void initPageButtons(int y) { for (int i = 0; i < this.pagesToShow; i++) { int xOffset = this.pagesToShow == 1 ? 0 : i == 0 - ? 32 : i == this.pagesToShow - 1 - ? -32 : 0; + ? 32 : i == this.pagesToShow - 1 + ? -32 : 0; // When we only show a single page, it's clearer to show something like // 'insert new page _before_ current' instead of just 'here'. @@ -219,7 +217,7 @@ protected TextArea createTextArea(int x, int y, int width, int height, } // Create an overflow handler for this specific edit box - OverflowHandler editBoxOverflowHandler = Scribble.config().overflowWhenTyping.get() + OverflowHandler editBoxOverflowHandler = Scribble.CONFIG.overflowWhenTyping.get() ? createOverflowHandlerForPage(pageOffset) : null; @@ -328,6 +326,12 @@ private void saveToFile(Path path) { bookFile.writeJson(path); } catch (Exception e) { Scribble.LOGGER.error("could not save book to file", e); + this.minecraft.setScreen(new AlertScreen( + () -> this.minecraft.setScreen(this), + Component.translatable("text.scribble.error.save_book.title"), + Component.translatable("text.scribble.error.summary_below") + .append("\n\n\n").append(ExceptionUtil.getExceptionSummary(e)) + )); } } @@ -336,7 +340,11 @@ private void loadFromFile(Path path) { BookFile bookFile = BookFile.readFile(path); this.pages.clear(); - this.pages.addAll(bookFile.pages().stream().map(RichText::fromFormattedString).toList()); + + //noinspection ConstantValue: this isn + if (bookFile.pages() != null) + this.pages.addAll(bookFile.pages().stream().map(RichText::fromFormattedString).toList()); + this.commandManager.clear(); this.dirty = true; @@ -344,6 +352,13 @@ private void loadFromFile(Path path) { this.updateCurrentPages(); } catch (Exception e) { Scribble.LOGGER.error("could not load book from file", e); + this.minecraft.setScreen(new AlertScreen( + () -> this.minecraft.setScreen(this), + Component.translatable("text.scribble.error.load_book.title"), + Component.translatable("text.scribble.error.load_book.subtitle") + .append("\n").append(Component.translatable("text.scribble.error.summary_below")) + .append("\n\n\n").append(ExceptionUtil.getExceptionSummary(e)) + )); } } @@ -384,7 +399,7 @@ public boolean keyPressed(KeyEvent keyEvent) { protected void init() { super.init(); - if (Scribble.config().showFormattingButtons.get()) { + if (Scribble.CONFIG.showFormattingButtons.get()) { int x = this.width / 2 + this.getBackgroundWidth() / 2 - 20; int y = this.getBackgroundY() + 12; diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java b/common/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java similarity index 90% rename from src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java rename to common/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java index bc6eca4..2629fcc 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java +++ b/common/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java @@ -4,7 +4,7 @@ import me.chrr.scribble.gui.PageNumberWidget; import me.chrr.scribble.gui.TextArea; import me.chrr.scribble.gui.button.IconButtonWidget; -import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.BookViewScreen; @@ -43,7 +43,7 @@ protected ScribbleBookScreen(Component title) { //region Widgets @Override protected void init() { - this.pagesToShow = Scribble.config().pagesToShow.get(); + this.pagesToShow = Scribble.CONFIG.pagesToShow.get(); if (this.pagesToShow == 1) { this.backgroundTexture = BookViewScreen.BOOK_LOCATION; } else { @@ -68,10 +68,10 @@ protected void init() { initMenuControls(getMenuControlsY()); this.backButton = addRenderableWidget(new PageButton(x + 43, y + 157, false, - (button) -> this.goPageBackward(this.minecraft.hasShiftDown()), this.shouldPlayTurnSound())); + (_) -> this.goPageBackward(this.minecraft.hasShiftDown()), this.shouldPlayTurnSound())); initPageButtons(y + 157); this.forwardButton = addRenderableWidget(new PageButton(x + 126 * this.pagesToShow - 10, y + 157, true, - (button) -> this.goPageForward(this.minecraft.hasShiftDown()), this.shouldPlayTurnSound())); + (_) -> this.goPageForward(this.minecraft.hasShiftDown()), this.shouldPlayTurnSound())); if (shouldShowActionButtons()) { initActionButtons(x, this.getBackgroundY() + 12 + 4); @@ -82,7 +82,7 @@ protected void init() { } private void initSettingsButton(int x, int y) { - MutableComponent settingsText = Component.literal("Scribble " + Scribble.platform().VERSION + "\n") + MutableComponent settingsText = Component.literal("Scribble " + Scribble.VERSION + "\n") .setStyle(Style.EMPTY.withBold(true)) .append(Component.translatable("text.scribble.action.settings") .setStyle(Style.EMPTY.withBold(false))); @@ -126,7 +126,7 @@ public void jumpToPage(int page) { public void showPage(int page, boolean insertIfMissing) { // Insert pages so the requested page exists if needed. - // (note that min+max instead of clamp here is deliberate) + // noinspection MathClampMigration: Math.clamp checks for min>max, which isn't necessarily true here. int newPage = Math.max(Math.min(page, this.getTotalPages() - 1), 0); if (insertIfMissing) { while (newPage < page && this.canInsertPages()) { @@ -179,11 +179,11 @@ public boolean keyPressed(KeyEvent event) { //region Rendering and dimensions @Override - public void renderBackground(GuiGraphics guiGraphics, int i, int j, float f) { - super.renderBackground(guiGraphics, i, j, f); + public void extractBackground(GuiGraphicsExtractor GuiGraphicsExtractor, int mouseX, int mouseY, float a) { + super.extractBackground(GuiGraphicsExtractor, mouseX, mouseY, a); int textureSize = this.pagesToShow == 1 ? 256 : 512; - guiGraphics.blit(RenderPipelines.GUI_TEXTURED, backgroundTexture, this.getBackgroundX(), this.getBackgroundY(), + GuiGraphicsExtractor.blit(RenderPipelines.GUI_TEXTURED, backgroundTexture, this.getBackgroundX(), this.getBackgroundY(), 0.0F, 0.0F, 122 * this.pagesToShow + 70, 192, textureSize, textureSize); } @@ -203,7 +203,7 @@ public int getBackgroundX() { } public int getBackgroundY() { - if (Scribble.config().centerBookGui.get()) { + if (Scribble.CONFIG.centerBookGui.get()) { // Perfect centering actually doesn't look great, so we put it on a third. return 2 + this.height / 3 - getMenuHeight() / 3; } else { diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java b/common/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java similarity index 84% rename from src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java rename to common/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java index 84e9493..37a2551 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java +++ b/common/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java @@ -1,14 +1,16 @@ package me.chrr.scribble.screen; +import me.chrr.scribble.ExceptionUtil; import me.chrr.scribble.Scribble; +import me.chrr.scribble.ScribbleConfig; import me.chrr.scribble.book.BookFile; import me.chrr.scribble.book.FileChooser; import me.chrr.scribble.book.RichText; -import me.chrr.scribble.ScribbleConfig; import me.chrr.scribble.gui.BookTextWidget; import me.chrr.scribble.gui.TextArea; import me.chrr.scribble.gui.button.IconButtonWidget; import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.AlertScreen; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.BookViewScreen; import net.minecraft.network.chat.ClickEvent; @@ -30,7 +32,7 @@ public ScribbleBookViewScreen(BookViewScreen.BookAccess book) { @Override protected boolean shouldShowActionButtons() { - return Scribble.config().showActionButtons.get() == ScribbleConfig.ShowActionButtons.ALWAYS; + return Scribble.CONFIG.showActionButtons.get() == ScribbleConfig.ShowActionButtons.ALWAYS; } @Override @@ -75,6 +77,12 @@ private void saveBookToFile() { bookFile.writeJson(path); } catch (Exception e) { Scribble.LOGGER.error("could not save book to file", e); + this.minecraft.setScreen(new AlertScreen( + () -> this.minecraft.setScreen(this), + Component.translatable("text.scribble.error.save_book.title"), + Component.translatable("text.scribble.error.summary_below") + .append("\n\n\n").append(ExceptionUtil.getExceptionSummary(e)) + )); } }); } diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java b/common/src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java similarity index 100% rename from src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java rename to common/src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java diff --git a/src/main/resources/assets/scribble/banner.png b/common/src/main/resources/assets/scribble/banner.png similarity index 100% rename from src/main/resources/assets/scribble/banner.png rename to common/src/main/resources/assets/scribble/banner.png diff --git a/src/main/resources/assets/scribble/icon.png b/common/src/main/resources/assets/scribble/icon.png similarity index 100% rename from src/main/resources/assets/scribble/icon.png rename to common/src/main/resources/assets/scribble/icon.png diff --git a/src/main/resources/assets/scribble/lang/de_de.json b/common/src/main/resources/assets/scribble/lang/de_de.json similarity index 100% rename from src/main/resources/assets/scribble/lang/de_de.json rename to common/src/main/resources/assets/scribble/lang/de_de.json diff --git a/src/main/resources/assets/scribble/lang/en_us.json b/common/src/main/resources/assets/scribble/lang/en_us.json similarity index 89% rename from src/main/resources/assets/scribble/lang/en_us.json rename to common/src/main/resources/assets/scribble/lang/en_us.json index 9f71b89..2643ed6 100644 --- a/src/main/resources/assets/scribble/lang/en_us.json +++ b/common/src/main/resources/assets/scribble/lang/en_us.json @@ -18,7 +18,6 @@ "config.scribble.value.paste_behavior.overflow": "Allow overflow to new pages", "config.scribble.category.miscellaneous": "Miscellaneous", "config.scribble.option.open_vanilla_book_screen_on_shift": "Open vanilla GUIs when holding SHIFT", - "text.tapestry.config.reset": "Reset to default value", "text.scribble.action.delete_page": "Delete this page", "text.scribble.action.insert_new_page": "Insert new page\nbefore this one", "text.scribble.action.insert_new_page_here": "Insert new page here", @@ -31,6 +30,10 @@ "text.scribble.overwrite_warning.description": "The current contents of this book will be lost!", "text.scribble.quit_without_saving.title": "Are you sure you want to quit without saving?", "text.scribble.quit_without_saving.description": "The changes made to this book will be lost!", + "text.scribble.error.save_book.title": "Something went wrong while saving the book!", + "text.scribble.error.load_book.title": "Something went wrong while loading the book!", + "text.scribble.error.load_book.subtitle": "Double-check if the file you're loading is correct.", + "text.scribble.error.summary_below": "The full error can be found in the log files, summary below.", "text.scribble.modifier.bold": "Bold", "text.scribble.modifier.italic": "Italic", "text.scribble.modifier.underline": "Underline", diff --git a/src/main/resources/assets/scribble/lang/es_mx.json b/common/src/main/resources/assets/scribble/lang/es_mx.json similarity index 100% rename from src/main/resources/assets/scribble/lang/es_mx.json rename to common/src/main/resources/assets/scribble/lang/es_mx.json diff --git a/src/main/resources/assets/scribble/lang/ru_ru.json b/common/src/main/resources/assets/scribble/lang/ru_ru.json similarity index 100% rename from src/main/resources/assets/scribble/lang/ru_ru.json rename to common/src/main/resources/assets/scribble/lang/ru_ru.json diff --git a/src/main/resources/assets/scribble/lang/tr_tr.json b/common/src/main/resources/assets/scribble/lang/tr_tr.json similarity index 100% rename from src/main/resources/assets/scribble/lang/tr_tr.json rename to common/src/main/resources/assets/scribble/lang/tr_tr.json diff --git a/src/main/resources/assets/scribble/lang/zh_cn.json b/common/src/main/resources/assets/scribble/lang/zh_cn.json similarity index 100% rename from src/main/resources/assets/scribble/lang/zh_cn.json rename to common/src/main/resources/assets/scribble/lang/zh_cn.json diff --git a/src/main/resources/assets/scribble/lang/zh_tw.json b/common/src/main/resources/assets/scribble/lang/zh_tw.json similarity index 100% rename from src/main/resources/assets/scribble/lang/zh_tw.json rename to common/src/main/resources/assets/scribble/lang/zh_tw.json diff --git a/src/main/resources/assets/scribble/textures/gui/book_2.png b/common/src/main/resources/assets/scribble/textures/gui/book_2.png similarity index 100% rename from src/main/resources/assets/scribble/textures/gui/book_2.png rename to common/src/main/resources/assets/scribble/textures/gui/book_2.png diff --git a/src/main/resources/assets/scribble/textures/gui/scribble_widgets.png b/common/src/main/resources/assets/scribble/textures/gui/scribble_widgets.png similarity index 100% rename from src/main/resources/assets/scribble/textures/gui/scribble_widgets.png rename to common/src/main/resources/assets/scribble/textures/gui/scribble_widgets.png diff --git a/src/main/resources/scribble.accesswidener b/common/src/main/resources/scribble.accesswidener similarity index 99% rename from src/main/resources/scribble.accesswidener rename to common/src/main/resources/scribble.accesswidener index 4e73e92..12130f7 100644 --- a/src/main/resources/scribble.accesswidener +++ b/common/src/main/resources/scribble.accesswidener @@ -1,4 +1,4 @@ -accessWidener v2 named +accessWidener v2 official # Expose fields and methods so we can override them in RichEditBoxWidget. accessible method net/minecraft/client/gui/components/MultiLineEditBox (Lnet/minecraft/client/gui/Font;IIIILnet/minecraft/network/chat/Component;Lnet/minecraft/network/chat/Component;IZIZZ)V#accessible field net/minecraft/client/gui/widget/EditBoxWidget editBox Lnet/minecraft/client/gui/EditBox; diff --git a/src/main/resources/scribble.mixins.json b/common/src/main/resources/scribble.mixins.json similarity index 100% rename from src/main/resources/scribble.mixins.json rename to common/src/main/resources/scribble.mixins.json diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index 37e32f4..a9ef16c 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -1,139 +1,14 @@ -import me.modmuss50.mpp.ReleaseType - -plugins { - id("dev.architectury.loom") - id("architectury-plugin") - id("com.gradleup.shadow") - id("me.modmuss50.mod-publish-plugin") -} - -fun Project.hasProp(namespace: String, key: String) = hasProperty("$namespace.$key") -fun Project.prop(namespace: String, key: String) = property("$namespace.$key") as String - -val common = requireNotNull(stonecutter.node.sibling("")).project - -val current = stonecutter.current.version -val minecraft = common.prop("minecraft", "version") - -version = "${common.prop("mod", "version")}+mc$current-fabric" -base.archivesName.set(prop("mod", "name")) - -architectury { - platformSetupLoomIde() - fabric() -} - -val commonBundle: Configuration by configurations.creating -val shadowBundle: Configuration by configurations.creating - -configurations { - compileClasspath.get().extendsFrom(commonBundle) - runtimeClasspath.get().extendsFrom(commonBundle) - get("developmentFabric").extendsFrom(commonBundle) -} - repositories { maven("https://maven.terraformersmc.com/releases/") - maven("https://maven.isxander.dev/releases") } dependencies { - minecraft("com.mojang:minecraft:$minecraft") - mappings(loom.officialMojangMappings()) - modImplementation("net.fabricmc:fabric-loader:${common.prop("fabric", "loaderVersion")}") - + val root = stonecutter.node.sibling("")!!.project fun fabricApiModule(name: String) = - modImplementation(fabricApi.module(name, common.prop("fabric", "apiVersion"))) - include(fabricApiModule("fabric-api-base")!!) - include(fabricApiModule("fabric-resource-loader-v1")!!) - - modCompileOnly("com.terraformersmc:modmenu:${common.prop("modmenu", "version")}") - - commonBundle(project(common.path, "namedElements")) { isTransitive = false } - shadowBundle(project(common.path, "transformProductionFabric")) { isTransitive = false } -} + implementation(fabricApi.module(name, root.property("fabric.api.version") as String)) -loom { - accessWidenerPath = common.project.loom.accessWidenerPath + jij(fabricApiModule("fabric-api-base")!!) + jij(fabricApiModule("fabric-resource-loader-v1")!!) - runConfigs.all { - isIdeConfigGenerated = false - runDir = "../../../run" - vmArgs("-Dmixin.debug.export=true") - } + compileOnly("com.terraformersmc:modmenu:18.0.0-alpha.5") } - -java { - targetCompatibility = JavaVersion.VERSION_21 - sourceCompatibility = JavaVersion.VERSION_21 -} - -tasks { - shadowJar { - configurations = listOf(shadowBundle) - archiveClassifier = "dev-shadow" - } - - remapJar { - injectAccessWidener = true - inputFile.set(shadowJar.get().archiveFile) - archiveClassifier = null - dependsOn(shadowJar) - } - - jar { - archiveClassifier = "dev" - } - - processResources { - // We construct our minecraft dependency string based on the versions provided in gradle.properties - val platformVersions = common.prop("platform", "versions").split(",") - val minecraftDependency = if (platformVersions.size == 1) - platformVersions.first() else ">=${platformVersions.first()} <=${platformVersions.last()}" - - filesMatching("fabric.mod.json") { - expand( - "version" to common.prop("mod", "version"), - "minecraft" to minecraftDependency, - ) - } - } - - register("buildAndCollect") { - group = "build" - from(remapJar.get().archiveFile) - into(rootProject.layout.buildDirectory.file("libs")) - dependsOn(build) - } - - publishMods.get().dependsOn("buildAndCollect") -} - -publishMods { - val versions = common.prop("platform", "versions").split(",") - val modVersion = common.prop("mod", "version") - changelog.set(providers.environmentVariable("CHANGELOG")) - - type = when { - modVersion.contains("alpha") -> ReleaseType.ALPHA - modVersion.contains("beta") -> ReleaseType.BETA - else -> ReleaseType.STABLE - } - - displayName.set("$modVersion - Fabric $minecraft") - version.set(project.version.toString()) - modLoaders.addAll(prop("platform", "loaders").split(",")) - file.set(tasks.remapJar.get().archiveFile) - - modrinth { - projectId.set(prop("modrinth", "id")) - accessToken.set(providers.environmentVariable("MODRINTH_TOKEN")) - minecraftVersions.addAll(versions) - } - - curseforge { - projectId.set(prop("curseforge", "id")) - accessToken.set(providers.environmentVariable("CURSEFORGE_TOKEN")) - minecraftVersions.addAll(versions) - } -} \ No newline at end of file diff --git a/fabric/gradle.properties b/fabric/gradle.properties deleted file mode 100644 index c7150fa..0000000 --- a/fabric/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -loom.platform=fabric -platform.loaders=fabric,quilt \ No newline at end of file diff --git a/fabric/src/main/java/me/chrr/scribble/fabric/ModMenuCompat.java b/fabric/src/main/java/me/chrr/scribble/fabric/ModMenuCompat.java index 8908ae0..d59aef0 100644 --- a/fabric/src/main/java/me/chrr/scribble/fabric/ModMenuCompat.java +++ b/fabric/src/main/java/me/chrr/scribble/fabric/ModMenuCompat.java @@ -3,9 +3,11 @@ import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; import me.chrr.scribble.Scribble; +import me.chrr.tapestry.gradle.annotation.FabricEntrypoint; import org.jspecify.annotations.NullMarked; @NullMarked +@FabricEntrypoint("modmenu") public class ModMenuCompat implements ModMenuApi { @Override public ConfigScreenFactory getModConfigScreenFactory() { diff --git a/fabric/src/main/java/me/chrr/scribble/fabric/ScribbleFabric.java b/fabric/src/main/java/me/chrr/scribble/fabric/ScribbleFabric.java index 7bd84fb..035b7cd 100644 --- a/fabric/src/main/java/me/chrr/scribble/fabric/ScribbleFabric.java +++ b/fabric/src/main/java/me/chrr/scribble/fabric/ScribbleFabric.java @@ -1,32 +1,15 @@ package me.chrr.scribble.fabric; import me.chrr.scribble.Scribble; +import me.chrr.tapestry.gradle.annotation.FabricEntrypoint; import net.fabricmc.api.ClientModInitializer; -import net.fabricmc.loader.api.FabricLoader; import org.jspecify.annotations.NullMarked; -import java.nio.file.Path; - @NullMarked -public class ScribbleFabric extends Scribble.Platform implements ClientModInitializer { +@FabricEntrypoint("client") +public class ScribbleFabric implements ClientModInitializer { @Override public void onInitializeClient() { - Scribble.init(this); - } - - @Override - protected String getModVersion() { - return FabricLoader.getInstance().getModContainer(Scribble.MOD_ID) - .orElseThrow().getMetadata().getVersion().getFriendlyString(); - } - - @Override - protected Path getConfigDir() { - return FabricLoader.getInstance().getConfigDir(); - } - - @Override - protected Path getGameDir() { - return FabricLoader.getInstance().getGameDir(); + Scribble.init(); } } \ No newline at end of file diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json deleted file mode 100644 index 36b9111..0000000 --- a/fabric/src/main/resources/fabric.mod.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "schemaVersion": 1, - "id": "scribble", - "version": "${version}", - "name": "Scribble", - "description": "Expertly edit your books with rich formatting options, page utilities and more!", - "authors": [ - "chrrrs" - ], - "contributors": [ - "WiskiWiski" - ], - "contact": { - "homepage": "https://modrinth.com/mod/scribble", - "sources": "https://github.com/chrrs/scribble", - "issues": "https://github.com/chrrs/scribble/issues" - }, - "license": "MIT", - "icon": "assets/scribble/icon.png", - "mixins": [ - "scribble.mixins.json" - ], - "environment": "client", - "entrypoints": { - "client": [ - "me.chrr.scribble.fabric.ScribbleFabric" - ], - "modmenu": [ - "me.chrr.scribble.fabric.ModMenuCompat" - ] - }, - "depends": { - "minecraft": "${minecraft}", - "fabricloader": ">=0.15", - "fabric-resource-loader-v1": "*" - }, - "breaks": { - "fixbookgui": "*", - "tapestry_config": "*" - } -} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 532690f..2af4f57 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,25 +1,9 @@ org.gradle.jvmargs=-Xmx1G -platform.versions=[versioned] -parchment.version=[versioned] - -# Mod details -mod.name=Scribble -mod.version=2.0.0-beta2 -mod.group=me.chrr - -# Fabric -# check these on https://modmuss50.me/fabric.html -fabric.loaderVersion=[versioned] -fabric.apiVersion=[versioned] - -# NeoForge -# check these on https://projects.neoforged.net/neoforged/neoforge +fabric.loader.version=[versioned] +fabric.api.version=[versioned] neoforge.version=[versioned] -# Dependencies -modmenu.version=11.0.2 +tapestry.version=[versioned] -# Distribution -modrinth.id=yXAvIk0x -curseforge.id=1051344 \ No newline at end of file +mod.version=2.0.2 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7aa90e9..7d8666c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sun May 12 20:30:28 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts index 48ffd19..e69de29 100644 --- a/neoforge/build.gradle.kts +++ b/neoforge/build.gradle.kts @@ -1,131 +0,0 @@ -import me.modmuss50.mpp.ReleaseType - -plugins { - id("dev.architectury.loom") - id("architectury-plugin") - id("com.gradleup.shadow") - id("me.modmuss50.mod-publish-plugin") -} - -fun Project.hasProp(namespace: String, key: String) = hasProperty("$namespace.$key") -fun Project.prop(namespace: String, key: String) = property("$namespace.$key") as String - -val common = requireNotNull(stonecutter.node.sibling("")).project - -val current = stonecutter.current.version -val minecraft = common.prop("minecraft", "version") - -version = "${common.prop("mod", "version")}+mc$current-neoforge" -base.archivesName.set(prop("mod", "name")) - -architectury { - platformSetupLoomIde() - neoForge() -} - -val commonBundle: Configuration by configurations.creating -val shadowBundle: Configuration by configurations.creating - -configurations { - compileClasspath.get().extendsFrom(commonBundle) - runtimeClasspath.get().extendsFrom(commonBundle) - get("developmentNeoForge").extendsFrom(commonBundle) -} - -repositories { - maven("https://maven.neoforged.net/releases/") -} - -dependencies { - minecraft("com.mojang:minecraft:$minecraft") - mappings(loom.officialMojangMappings()) - neoForge("net.neoforged:neoforge:${common.prop("neoforge", "version")}") - - commonBundle(project(common.path, "namedElements")) { isTransitive = false } - shadowBundle(project(common.path, "transformProductionNeoForge")) { isTransitive = false } -} - -loom { - accessWidenerPath = common.project.loom.accessWidenerPath - - runConfigs.all { - isIdeConfigGenerated = false - runDir = "../../../run" - vmArgs("-Dmixin.debug.export=true") - } -} - -java { - targetCompatibility = JavaVersion.VERSION_21 - sourceCompatibility = JavaVersion.VERSION_21 -} - -tasks { - shadowJar { - configurations = listOf(shadowBundle) - archiveClassifier = "dev-shadow" - } - - remapJar { - atAccessWideners.add("scribble.accesswidener") - inputFile.set(shadowJar.get().archiveFile) - archiveClassifier = null - dependsOn(shadowJar) - } - - jar { - archiveClassifier = "dev" - } - - processResources { - // We construct our minecraft dependency string based on the versions provided in gradle.properties - val platformVersions = common.prop("platform", "versions").split(",") - val minecraftDependency = if (platformVersions.size == 1) - "[${platformVersions.first()}]" else "[${platformVersions.first()}, ${platformVersions.last()}]" - - filesMatching("META-INF/neoforge.mods.toml") { - expand( - "version" to common.prop("mod", "version"), - "minecraft" to minecraftDependency, - ) - } - } - - register("buildAndCollect") { - group = "build" - from(remapJar.get().archiveFile) - into(rootProject.layout.buildDirectory.file("libs")) - dependsOn(build) - } - - publishMods.get().dependsOn("buildAndCollect") -} - -publishMods { - val versions = common.prop("platform", "versions").split(",") - val modVersion = common.prop("mod", "version") - changelog.set(providers.environmentVariable("CHANGELOG")) - - type = when { - modVersion.contains("alpha") -> ReleaseType.ALPHA - modVersion.contains("beta") -> ReleaseType.BETA - else -> ReleaseType.STABLE - } - - displayName.set("$modVersion - NeoForge $minecraft") - version.set(project.version.toString()) - modLoaders.addAll(prop("platform", "loaders").split(",")) - file.set(tasks.remapJar.get().archiveFile) - - modrinth { - projectId.set(prop("modrinth", "id")) - accessToken.set(providers.environmentVariable("MODRINTH_TOKEN")) - minecraftVersions.addAll(versions) - } - - curseforge { - projectId.set(prop("curseforge", "id")) - accessToken.set(providers.environmentVariable("CURSEFORGE_TOKEN")) - minecraftVersions.addAll(versions) - } -} \ No newline at end of file diff --git a/neoforge/gradle.properties b/neoforge/gradle.properties deleted file mode 100644 index 8fb23d3..0000000 --- a/neoforge/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -loom.platform=neoforge -platform.loaders=neoforge \ No newline at end of file diff --git a/neoforge/src/main/java/me/chrr/scribble/neoforge/ScribbleNeoForge.java b/neoforge/src/main/java/me/chrr/scribble/neoforge/ScribbleNeoForge.java index 66f6ac7..405d167 100644 --- a/neoforge/src/main/java/me/chrr/scribble/neoforge/ScribbleNeoForge.java +++ b/neoforge/src/main/java/me/chrr/scribble/neoforge/ScribbleNeoForge.java @@ -3,36 +3,17 @@ import me.chrr.scribble.Scribble; import net.neoforged.api.distmarker.Dist; import net.neoforged.fml.ModContainer; -import net.neoforged.fml.ModList; import net.neoforged.fml.common.Mod; -import net.neoforged.fml.loading.FMLPaths; import net.neoforged.neoforge.client.gui.IConfigScreenFactory; import org.jspecify.annotations.NullMarked; -import java.nio.file.Path; - @NullMarked @Mod(value = Scribble.MOD_ID, dist = Dist.CLIENT) -public class ScribbleNeoForge extends Scribble.Platform { +public class ScribbleNeoForge { public ScribbleNeoForge(ModContainer mod) { - Scribble.init(this); + Scribble.init(); mod.registerExtensionPoint(IConfigScreenFactory.class, - (container, parent) -> Scribble.buildConfigScreen(parent)); - } - - @Override - protected String getModVersion() { - return ModList.get().getModFileById(Scribble.MOD_ID).versionString(); - } - - @Override - protected Path getConfigDir() { - return FMLPaths.CONFIGDIR.get(); - } - - @Override - protected Path getGameDir() { - return FMLPaths.GAMEDIR.get(); + (_, parent) -> Scribble.buildConfigScreen(parent)); } } \ No newline at end of file diff --git a/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/main/resources/META-INF/neoforge.mods.toml deleted file mode 100644 index 33afa56..0000000 --- a/neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ /dev/null @@ -1,21 +0,0 @@ -modLoader = "javafml" -loaderVersion = "[1,)" -issueTrackerURL = "https://github.com/chrrs/scribble/issues" -license = "MIT" - -[[mods]] -modId = "scribble" -version = "${version}" -displayName = "Scribble" -description = "Expertly edit your books with rich formatting options, page utilities and more!" -authors = "chrrrs" -credits = "WiskiWiski" -logoFile = "assets/scribble/banner.png" -logoBlur = false - -[[mixins]] -config = "scribble.mixins.json" - -[[dependencies.scribble]] -modId = "minecraft" -versionRange = "${minecraft}" \ No newline at end of file diff --git a/neoforge/src/main/resources/pack.mcmeta b/neoforge/src/main/resources/pack.mcmeta deleted file mode 100644 index 995590b..0000000 --- a/neoforge/src/main/resources/pack.mcmeta +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pack": { - "description": "Scribble", - "pack_format": 15 - } -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b09220b..23b4b27 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,15 +1,14 @@ pluginManagement { repositories { maven("https://maven.fabricmc.net/") - maven("https://maven.architectury.dev") - maven("https://maven.neoforged.net/releases/") maven("https://maven.kikugie.dev/snapshots") + maven("https://maven.chrr.me/releases") gradlePluginPortal() } } plugins { - id("dev.kikugie.stonecutter") version "0.8.1" + id("dev.kikugie.stonecutter") version "0.9.2" } stonecutter { @@ -17,9 +16,11 @@ stonecutter { centralScript = "build.gradle.kts" create(rootProject) { - versions("1.21.11") - branch("fabric") { versions("1.21.11") } - branch("neoforge") { versions("1.21.11") } + versions("26.1") + + branch("common") + branch("fabric") + branch("neoforge") } } diff --git a/src/main/java/me/chrr/scribble/Scribble.java b/src/main/java/me/chrr/scribble/Scribble.java deleted file mode 100644 index fd9ee2f..0000000 --- a/src/main/java/me/chrr/scribble/Scribble.java +++ /dev/null @@ -1,62 +0,0 @@ -package me.chrr.scribble; - -import me.chrr.scribble.book.FileChooser; -import me.chrr.tapestry.config.ReflectedConfig; -import me.chrr.tapestry.config.gui.TapestryConfigScreen; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.resources.Identifier; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -import java.nio.file.Path; -import java.util.List; -import java.util.Objects; - -@NullMarked -public class Scribble { - public static final String MOD_ID = "scribble"; - public static final Logger LOGGER = LogManager.getLogger(); - - private static @Nullable Platform PLATFORM; - private static @Nullable ScribbleConfig CONFIG; - - public static void init(Platform platform) { - PLATFORM = platform; - - CONFIG = ReflectedConfig.load(() -> platform.CONFIG_DIR, ScribbleConfig.class, - "scribble.client.json", List.of("scribble.json")); - - FileChooser.convertLegacyBooks(); - } - - public static Screen buildConfigScreen(Screen parent) { - return new TapestryConfigScreen(parent, config()); - } - - public static ScribbleConfig config() { - return Objects.requireNonNull(CONFIG); - } - - public static Platform platform() { - return Objects.requireNonNull(PLATFORM); - } - - public static Identifier id(String path) { - return Identifier.fromNamespaceAndPath(MOD_ID, path); - } - - public abstract static class Platform { - public final Path CONFIG_DIR = getConfigDir(); - public final Path BOOK_DIR = getGameDir().resolve("books"); - - public final String VERSION = getModVersion(); - - protected abstract String getModVersion(); - - protected abstract Path getConfigDir(); - - protected abstract Path getGameDir(); - } -} diff --git a/src/main/java/me/chrr/tapestry/config/Config.java b/src/main/java/me/chrr/tapestry/config/Config.java deleted file mode 100644 index 5a4b1c4..0000000 --- a/src/main/java/me/chrr/tapestry/config/Config.java +++ /dev/null @@ -1,16 +0,0 @@ -package me.chrr.tapestry.config; - -import net.minecraft.network.chat.Component; -import org.jspecify.annotations.Nullable; - -import java.util.Collection; - -public interface Config { - Collection> getOptions(); - - Component getTitle(); - - ConfigIo.@Nullable UpgradeRewriter getUpgradeRewriter(); - - void save(); -} diff --git a/src/main/java/me/chrr/tapestry/config/ConfigEnvironment.java b/src/main/java/me/chrr/tapestry/config/ConfigEnvironment.java deleted file mode 100644 index 6a8fcd1..0000000 --- a/src/main/java/me/chrr/tapestry/config/ConfigEnvironment.java +++ /dev/null @@ -1,7 +0,0 @@ -package me.chrr.tapestry.config; - -import java.nio.file.Path; - -public interface ConfigEnvironment { - Path getConfigDir(); -} diff --git a/src/main/java/me/chrr/tapestry/config/ConfigIo.java b/src/main/java/me/chrr/tapestry/config/ConfigIo.java deleted file mode 100644 index 9e84013..0000000 --- a/src/main/java/me/chrr/tapestry/config/ConfigIo.java +++ /dev/null @@ -1,108 +0,0 @@ -package me.chrr.tapestry.config; - -import com.google.gson.*; -import me.chrr.tapestry.config.value.Value; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.jspecify.annotations.NullMarked; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; - -@NullMarked -public enum ConfigIo { - ; - - private static final Logger LOGGER = LogManager.getLogger("Tapestry/ConfigIo"); - - public static final Gson GSON = new GsonBuilder() - .setStrictness(Strictness.LENIENT) - .setFormattingStyle(FormattingStyle.PRETTY) - .create(); - - public static void loadFromPathOrSaveDefault(Config config, Path file, List aliases) { - if (Files.exists(file)) { - loadFromPath(config, file); - return; - } else { - for (Path alias : aliases) { - if (!Files.isRegularFile(alias)) - continue; - - loadFromPath(config, alias); - saveToPath(config, file); - - try { - Files.delete(alias); - } catch (IOException e) { - LOGGER.error("Failed to delete old config file at '{}'", alias, e); - } - - LOGGER.info("Migrated config from '{}' to '{}'", alias, file); - return; - } - } - - LOGGER.info("No config file found, saving default config to '{}'", file); - saveToPath(config, file); - } - - public static void loadFromPath(Config config, Path path) { - try { - JsonObject object = JsonParser.parseString(Files.readString(path)).getAsJsonObject(); - - UpgradeRewriter upgradeRewriter = config.getUpgradeRewriter(); - if (upgradeRewriter != null && object.has("version")) { - int version = object.get("version").getAsInt(); - upgradeRewriter.upgrade(version, object); - object.addProperty("version", upgradeRewriter.getLatestVersion()); - LOGGER.info("Upgraded config at '{}' to version {}", path, upgradeRewriter.getLatestVersion()); - - String json = GSON.toJson(object); - Files.writeString(path, json); - } - - for (Option option : config.getOptions()) { - if (option.serializedName != null && object.has(option.serializedName)) { - readIntoValue(option.value, object.get(option.serializedName)); - } - } - } catch (IOException e) { - LOGGER.error("Failed to load config from '{}'", path, e); - } - } - - public static void readIntoValue(Value value, JsonElement element) { - value.set(GSON.fromJson(element, value.getValueType())); - } - - public static void saveToPath(Config config, Path path) { - JsonObject object = new JsonObject(); - - UpgradeRewriter upgradeRewriter = config.getUpgradeRewriter(); - if (upgradeRewriter != null) { - object.addProperty("version", upgradeRewriter.getLatestVersion()); - } - - for (Option option : config.getOptions()) { - if (option.serializedName != null) { - object.add(option.serializedName, GSON.toJsonTree(option.value.get(), option.value.getValueType())); - } - } - - try { - String json = GSON.toJson(object); - Files.writeString(path, json); - } catch (IOException e) { - LOGGER.error("Failed to save config to '{}'", path, e); - } - } - - public interface UpgradeRewriter { - void upgrade(int fromVersion, JsonObject config); - - int getLatestVersion(); - } -} diff --git a/src/main/java/me/chrr/tapestry/config/NamingStrategy.java b/src/main/java/me/chrr/tapestry/config/NamingStrategy.java deleted file mode 100644 index 806dd8a..0000000 --- a/src/main/java/me/chrr/tapestry/config/NamingStrategy.java +++ /dev/null @@ -1,40 +0,0 @@ -package me.chrr.tapestry.config; - -import org.jspecify.annotations.NullMarked; - -import java.util.Arrays; - -@NullMarked -public enum NamingStrategy { - KEEP, - SNAKE_CASE, - CAMEL_CASE; - - public String transform(String name) { - return switch (this) { - case KEEP -> name; - case SNAKE_CASE -> String.join("_", splitName(name)).toLowerCase(); - case CAMEL_CASE -> { - StringBuilder builder = new StringBuilder(); - String[] words = splitName(name); - builder.append(words[0]); - Arrays.stream(words).skip(1).forEach((word) -> builder.append(capitalize(word))); - yield builder.toString(); - } - }; - } - - private static String[] splitName(String name) { - return name - .replaceAll("[^\\p{Alnum}]+", " ") - .replaceAll("([\\p{Lower}\\d])(\\p{Upper})", "$1 $2") - .replaceAll("(\\p{Upper}+)(\\p{Upper}\\p{Lower})", "$1 $2") - .replaceAll("(\\p{Alpha})(\\d)|(\\d)(\\p{Alpha})", "$1$3 $2$4") - .trim() - .split("\\s+"); - } - - private static String capitalize(String s) { - return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); - } -} diff --git a/src/main/java/me/chrr/tapestry/config/Option.java b/src/main/java/me/chrr/tapestry/config/Option.java deleted file mode 100644 index dbf61fb..0000000 --- a/src/main/java/me/chrr/tapestry/config/Option.java +++ /dev/null @@ -1,20 +0,0 @@ -package me.chrr.tapestry.config; - -import me.chrr.tapestry.config.value.Value; -import net.minecraft.network.chat.Component; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -@NullMarked -public class Option { - public @Nullable String serializedName; - public Value value; - - public Component displayName = Component.empty(); - public @Nullable Component header; - public boolean hidden; - - public Option(Value value) { - this.value = value; - } -} diff --git a/src/main/java/me/chrr/tapestry/config/ReflectedConfig.java b/src/main/java/me/chrr/tapestry/config/ReflectedConfig.java deleted file mode 100644 index c2f0aa2..0000000 --- a/src/main/java/me/chrr/tapestry/config/ReflectedConfig.java +++ /dev/null @@ -1,206 +0,0 @@ -package me.chrr.tapestry.config; - -import com.google.gson.JsonObject; -import me.chrr.tapestry.config.annotation.*; -import me.chrr.tapestry.config.value.Constraint; -import me.chrr.tapestry.config.value.TrackedValue; -import me.chrr.tapestry.config.value.Value; -import me.chrr.tapestry.config.value.VirtualValue; -import net.minecraft.network.chat.Component; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -import java.lang.reflect.*; -import java.nio.file.Path; -import java.util.*; -import java.util.function.Function; - -@NullMarked -public abstract class ReflectedConfig implements Config { - private final List> options = new ArrayList<>(); - - private Component title = Component.empty(); - private ConfigIo.@Nullable UpgradeRewriter upgradeRewriter = null; - - private @Nullable Path currentConfigPath = null; - private @Nullable TranslationPrefix translationPrefix = null; - - //region Value constructors - protected Value value(T defaultValue) { - //noinspection unchecked: this should be correct. - return new TrackedValue<>((Class) defaultValue.getClass(), defaultValue); - } - - protected Value map(Value value, Function aToB, Function bToA) { - V defaultValue = aToB.apply(value.getDefaultValue()); - - //noinspection unchecked: this should be correct. - return new VirtualValue<>((Class) defaultValue.getClass(), defaultValue, - () -> aToB.apply(value.get()), (v) -> value.set(bToA.apply(v))); - } - //endregion - - //region Initialization & reflection - private void reflectOptions() { - this.upgradeRewriter = reflectUpgradeRewriter(); - this.translationPrefix = getClass().getAnnotation(TranslationPrefix.class); - - // Get the default naming strategy. - NamingStrategy namingStrategy = NamingStrategy.SNAKE_CASE; - SerializeName.Strategy serializeNameStrategyAnnotation = getClass().getAnnotation(SerializeName.Strategy.class); - if (serializeNameStrategyAnnotation != null) - namingStrategy = serializeNameStrategyAnnotation.value(); - - // Get the config screen title. - this.title = getTranslatedName("title"); - - // Construct options for all public, non-static, non-transient fields. - for (Field field : getClass().getFields()) { - if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) - continue; - this.options.add(reflectOptionFromField(field, namingStrategy)); - } - } - - private ConfigIo.@Nullable UpgradeRewriter reflectUpgradeRewriter() { - ConfigIo.UpgradeRewriter upgradeRewriter = null; - - for (Method method : getClass().getDeclaredMethods()) { - UpgradeRewriter annotation = method.getAnnotation(UpgradeRewriter.class); - if (annotation == null) - continue; - - if (!Modifier.isStatic(method.getModifiers())) - throw new IllegalArgumentException("Upgrade rewriter '" + method.getName() + "' is not static"); - - if (upgradeRewriter != null) - throw new IllegalArgumentException("Config class '" + getClass().getName() + "' defines more than one upgrade rewriter"); - - method.setAccessible(true); - upgradeRewriter = new ConfigIo.UpgradeRewriter() { - @Override - public void upgrade(int fromVersion, JsonObject config) { - try { - method.invoke(null, fromVersion, config); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException("Failed to invoke upgrade rewriter", e); - } - } - - @Override - public int getLatestVersion() { - return annotation.currentVersion(); - } - }; - } - - return upgradeRewriter; - } - - private Option reflectOptionFromField(Field field, NamingStrategy defaultNamingStrategy) { - try { - Class type = field.getType(); - if (!Value.class.isAssignableFrom(type)) - throw new IllegalArgumentException("All (non-transient) public fields in a config class should be Value, which " + type + " isn't"); - - Value value = (Value) field.get(this); - return reflectOptionFromValue(value, field, defaultNamingStrategy); - } catch (IllegalAccessException e) { - throw new RuntimeException("Couldn't initialize config field " + field); - } - } - - private Option reflectOptionFromValue(Value value, Field field, NamingStrategy defaultNamingStrategy) { - Option option = new Option<>(value); - - SerializeName serializeName = field.getAnnotation(SerializeName.class); - SerializeName.Strategy strategy = getClass().getAnnotation(SerializeName.Strategy.class); - NamingStrategy namingStrategy = strategy != null ? strategy.value() : defaultNamingStrategy; - - // Serialization options. - if (value instanceof TrackedValue trackedValue) { - option.serializedName = serializeName != null ? serializeName.value() : namingStrategy.transform(field.getName()); - - // Fill in some value properties if it's an enum. - Class valueType = trackedValue.getValueType(); - if (valueType.isEnum()) { - if (value.constraint == null) - value.constraint = new Constraint.Values<>(Arrays.asList(valueType.getEnumConstants())); - - if (!value.didSetTextProvider) - value.textProvider = (v) -> getTranslatedName( - "value." + namingStrategy.transform(valueType.getSimpleName()) - + "." + namingStrategy.transform(v.toString())); - } - } - - // GUI-specific options. - if (field.isAnnotationPresent(Hidden.class)) { - option.hidden = true; - } else { - option.displayName = getTranslatedName("option." + namingStrategy.transform(field.getName())); - } - - Header header = field.getAnnotation(Header.class); - if (header != null) - option.header = getTranslatedName("category." + header.value()); - - return option; - } - - private Component getTranslatedName(String key) { - if (this.translationPrefix == null) { - return Component.literal(key); - } else { - return Component.translatable(this.translationPrefix.value() + "." + key); - } - } - //endregion - - //region Saving & loading - @Override - public void save() { - ConfigIo.saveToPath(this, Objects.requireNonNull(this.currentConfigPath)); - } - - public static T load(ConfigEnvironment environment, Class configClass, String file, List aliases) { - try { - Constructor constructor = configClass.getConstructor(); - constructor.setAccessible(true); - - T config = constructor.newInstance(); - ((ReflectedConfig) config).reflectOptions(); - - Path confDir = environment.getConfigDir(); - Path configFile = confDir.resolve(file); - ((ReflectedConfig) config).currentConfigPath = configFile; - - ConfigIo.loadFromPathOrSaveDefault(config, configFile, - aliases.stream().map(confDir::resolve).toList()); - - return config; - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException("Config class '" + configClass.getName() + "' does not have a default constructor", e); - } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { - throw new IllegalArgumentException("Config class '" + configClass.getName() + "' could not be instantiated", e); - } - } - //endregion - - //region Interface implementations - @Override - public Collection> getOptions() { - return this.options; - } - - @Override - public Component getTitle() { - return this.title; - } - - @Override - public ConfigIo.@Nullable UpgradeRewriter getUpgradeRewriter() { - return this.upgradeRewriter; - } - //endregion -} diff --git a/src/main/java/me/chrr/tapestry/config/annotation/Header.java b/src/main/java/me/chrr/tapestry/config/annotation/Header.java deleted file mode 100644 index 8ee7a3e..0000000 --- a/src/main/java/me/chrr/tapestry/config/annotation/Header.java +++ /dev/null @@ -1,15 +0,0 @@ -package me.chrr.tapestry.config.annotation; - -import org.jspecify.annotations.NullMarked; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@NullMarked -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface Header { - String value(); -} diff --git a/src/main/java/me/chrr/tapestry/config/annotation/Hidden.java b/src/main/java/me/chrr/tapestry/config/annotation/Hidden.java deleted file mode 100644 index c751fb0..0000000 --- a/src/main/java/me/chrr/tapestry/config/annotation/Hidden.java +++ /dev/null @@ -1,14 +0,0 @@ -package me.chrr.tapestry.config.annotation; - -import org.jspecify.annotations.NullMarked; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@NullMarked -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface Hidden { -} diff --git a/src/main/java/me/chrr/tapestry/config/annotation/SerializeName.java b/src/main/java/me/chrr/tapestry/config/annotation/SerializeName.java deleted file mode 100644 index 1c3bea5..0000000 --- a/src/main/java/me/chrr/tapestry/config/annotation/SerializeName.java +++ /dev/null @@ -1,22 +0,0 @@ -package me.chrr.tapestry.config.annotation; - -import me.chrr.tapestry.config.NamingStrategy; -import org.jspecify.annotations.NullMarked; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@NullMarked -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface SerializeName { - String value(); - - @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.TYPE, ElementType.FIELD}) - @interface Strategy { - NamingStrategy value(); - } -} diff --git a/src/main/java/me/chrr/tapestry/config/annotation/TranslationPrefix.java b/src/main/java/me/chrr/tapestry/config/annotation/TranslationPrefix.java deleted file mode 100644 index ec5a024..0000000 --- a/src/main/java/me/chrr/tapestry/config/annotation/TranslationPrefix.java +++ /dev/null @@ -1,15 +0,0 @@ -package me.chrr.tapestry.config.annotation; - -import org.jspecify.annotations.NullMarked; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@NullMarked -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface TranslationPrefix { - String value(); -} diff --git a/src/main/java/me/chrr/tapestry/config/annotation/UpgradeRewriter.java b/src/main/java/me/chrr/tapestry/config/annotation/UpgradeRewriter.java deleted file mode 100644 index b162f79..0000000 --- a/src/main/java/me/chrr/tapestry/config/annotation/UpgradeRewriter.java +++ /dev/null @@ -1,15 +0,0 @@ -package me.chrr.tapestry.config.annotation; - -import org.jspecify.annotations.NullMarked; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@NullMarked -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface UpgradeRewriter { - int currentVersion(); -} diff --git a/src/main/java/me/chrr/tapestry/config/gui/IconButton.java b/src/main/java/me/chrr/tapestry/config/gui/IconButton.java deleted file mode 100644 index d44a21b..0000000 --- a/src/main/java/me/chrr/tapestry/config/gui/IconButton.java +++ /dev/null @@ -1,52 +0,0 @@ -package me.chrr.tapestry.config.gui; - -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.components.AbstractButton; -import net.minecraft.client.gui.components.Tooltip; -import net.minecraft.client.gui.narration.NarrationElementOutput; -import net.minecraft.client.input.InputWithModifiers; -import net.minecraft.client.renderer.RenderPipelines; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.Identifier; -import net.minecraft.util.ARGB; -import net.minecraft.util.CommonColors; -import org.jspecify.annotations.NullMarked; - -import java.util.function.Consumer; - -@NullMarked -public class IconButton extends AbstractButton { - private final Consumer onPress; - private final Identifier icon; - - public IconButton(int x, int y, int width, int height, Component tooltip, Identifier icon, Consumer onPress) { - super(x, y, width, height, tooltip); - this.onPress = onPress; - this.icon = icon; - - setTooltip(Tooltip.create(tooltip)); - } - - @Override - public void onPress(InputWithModifiers input) { - this.onPress.accept(this); - } - - @Override - protected void renderContents(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { - this.renderDefaultSprite(guiGraphics); - - int color = this.isActive() ? CommonColors.WHITE : CommonColors.LIGHT_GRAY; - int shadow = ARGB.scaleRGB(color, 0.25F); - - int x = getX() + getWidth() / 2 - 16 / 2; - int y = getY() + getHeight() / 2 - 16 / 2; - guiGraphics.blit(RenderPipelines.GUI_TEXTURED, icon, x + 1, y + 1, 0, 0, 16, 16, 16, 16, shadow); - guiGraphics.blit(RenderPipelines.GUI_TEXTURED, icon, x, y, 0, 0, 16, 16, 16, 16, color); - } - - @Override - protected void updateWidgetNarration(NarrationElementOutput output) { - this.defaultButtonNarrationText(output); - } -} diff --git a/src/main/java/me/chrr/tapestry/config/gui/OptionList.java b/src/main/java/me/chrr/tapestry/config/gui/OptionList.java deleted file mode 100644 index 9b815e9..0000000 --- a/src/main/java/me/chrr/tapestry/config/gui/OptionList.java +++ /dev/null @@ -1,142 +0,0 @@ -package me.chrr.tapestry.config.gui; - -import me.chrr.tapestry.config.Option; -import me.chrr.tapestry.config.gui.widget.*; -import me.chrr.tapestry.config.value.Constraint; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.components.ContainerObjectSelectionList; -import net.minecraft.client.gui.components.StringWidget; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.narration.NarratableEntry; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.renderer.RenderPipelines; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.Identifier; -import org.jspecify.annotations.NullMarked; - -import java.util.List; - -@NullMarked -public class OptionList extends ContainerObjectSelectionList { - private final boolean showHeaderSeparator; - - public OptionList(Minecraft minecraft, boolean showHeaderSeparator, int width, int height, int y) { - super(minecraft, width, height, y, 25); - - this.showHeaderSeparator = showHeaderSeparator; - this.centerListVertically = false; - } - - public void addHeader(Component text) { - int padding = this.children().isEmpty() ? 8 : 16; - int height = padding + OptionList.this.minecraft.font.lineHeight; - this.addEntry(new HeaderEntry(text), height); - } - - public OptionProxy addOption(Option option) { - OptionProxy proxy = new OptionProxy<>(option); - this.addEntry(new OptionEntry<>(proxy)); - return proxy; - } - - @Override - public int getRowWidth() { - return 310; - } - - @Override - protected void renderListSeparators(GuiGraphics guiGraphics) { - if (this.showHeaderSeparator) { - Identifier headerSeparator = this.minecraft.level == null ? Screen.HEADER_SEPARATOR : Screen.INWORLD_HEADER_SEPARATOR; - guiGraphics.blit(RenderPipelines.GUI_TEXTURED, headerSeparator, this.getX(), this.getY() - 2, 0f, 0f, this.getWidth(), 2, 32, 2); - } - - Identifier footerSeparator = this.minecraft.level == null ? Screen.FOOTER_SEPARATOR : Screen.INWORLD_FOOTER_SEPARATOR; - guiGraphics.blit(RenderPipelines.GUI_TEXTURED, footerSeparator, this.getX(), this.getBottom(), 0f, 0f, this.getWidth(), 2, 32, 2); - } - - private static OptionWidget getWidgetForProxy(OptionProxy optionProxy) { - Class valueClass = optionProxy.option.value.getValueType(); - - if (valueClass == boolean.class || valueClass == Boolean.class) { - return unsafeCast(new BooleanOptionWidget(unsafeCast(optionProxy))); - } else if (optionProxy.option.value.constraint instanceof Constraint.Range range && range.step() != null) { - if ((valueClass == int.class || valueClass == Integer.class)) { - return unsafeCast(new SliderOptionWidget.Int(unsafeCast(optionProxy), unsafeCast(range))); - } else if ((valueClass == float.class || valueClass == Float.class)) { - return unsafeCast(new SliderOptionWidget.Float(unsafeCast(optionProxy), unsafeCast(range))); - } else { - throw new IllegalArgumentException("Range constraint can't be applied to a value of type " + valueClass); - } - } else if (optionProxy.option.value.constraint instanceof Constraint.Values(List values)) { - return new EnumOptionWidget<>(optionProxy, values); - } else { - return new ReadOnlyOptionWidget<>(optionProxy); - } - } - - @SuppressWarnings("unchecked") - private static T unsafeCast(V value) { - return (T) value; - } - - protected abstract static class Entry extends ContainerObjectSelectionList.Entry { - } - - protected class HeaderEntry extends Entry { - private final StringWidget widget; - - public HeaderEntry(Component text) { - this.widget = new StringWidget(text, minecraft.font); - } - - public List narratables() { - return List.of(this.widget); - } - - public void renderContent(GuiGraphics graphics, int x, int y, boolean bl, float delta) { - this.widget.setPosition(this.getContentX() + 2, this.getContentBottom() - minecraft.font.lineHeight); - this.widget.render(graphics, x, y, delta); - } - - public List children() { - return List.of(this.widget); - } - } - - protected static class OptionEntry extends Entry { - private final OptionWidget widget; - private final OptionProxy optionProxy; - private final IconButton reset; - - public OptionEntry(OptionProxy optionProxy) { - this.widget = getWidgetForProxy(optionProxy); - this.optionProxy = optionProxy; - - this.reset = new IconButton(0, 0, 0, 0, - Component.translatable("text.tapestry.config.reset"), - Identifier.fromNamespaceAndPath("tapestry-config", "textures/gui/reset.png"), - (button) -> optionProxy.reset()); - } - - public List narratables() { - return List.of(this.widget, this.reset); - } - - public void renderContent(GuiGraphics graphics, int x, int y, boolean isHovering, float delta) { - this.widget.setPosition(this.getContentX(), this.getContentY()); - this.widget.setSize(this.getContentWidth() - this.getContentHeight() - 4, this.getContentHeight()); - this.widget.render(graphics, x, y, delta); - - this.reset.active = this.optionProxy.isChanged(); - this.reset.setPosition(this.getContentRight() - this.getContentHeight(), this.getContentY()); - this.reset.setSize(this.getContentHeight(), this.getContentHeight()); - this.reset.render(graphics, x, y, delta); - } - - public List children() { - return List.of(this.widget, this.reset); - } - } -} diff --git a/src/main/java/me/chrr/tapestry/config/gui/OptionProxy.java b/src/main/java/me/chrr/tapestry/config/gui/OptionProxy.java deleted file mode 100644 index ac74dd2..0000000 --- a/src/main/java/me/chrr/tapestry/config/gui/OptionProxy.java +++ /dev/null @@ -1,27 +0,0 @@ -package me.chrr.tapestry.config.gui; - -import me.chrr.tapestry.config.Option; -import org.jspecify.annotations.NullMarked; - -@NullMarked -public class OptionProxy { - public final Option option; - public T value; - - public OptionProxy(Option option) { - this.option = option; - this.value = option.value.get(); - } - - public void apply() { - this.option.value.set(this.value); - } - - public void reset() { - this.value = this.option.value.getDefaultValue(); - } - - public boolean isChanged() { - return this.value != this.option.value.getDefaultValue(); - } -} diff --git a/src/main/java/me/chrr/tapestry/config/gui/TapestryConfigScreen.java b/src/main/java/me/chrr/tapestry/config/gui/TapestryConfigScreen.java deleted file mode 100644 index 89169fe..0000000 --- a/src/main/java/me/chrr/tapestry/config/gui/TapestryConfigScreen.java +++ /dev/null @@ -1,140 +0,0 @@ -package me.chrr.tapestry.config.gui; - -import me.chrr.tapestry.config.Config; -import me.chrr.tapestry.config.Option; -import net.minecraft.client.gui.components.AbstractWidget; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.components.tabs.Tab; -import net.minecraft.client.gui.components.tabs.TabManager; -import net.minecraft.client.gui.components.tabs.TabNavigationBar; -import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; -import net.minecraft.client.gui.layouts.LinearLayout; -import net.minecraft.client.gui.navigation.ScreenRectangle; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.CommonComponents; -import net.minecraft.network.chat.Component; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -@NullMarked -public class TapestryConfigScreen extends Screen { - private final Config[] configs; - private final Screen parent; - - private final List> proxies = new ArrayList<>(); - - private final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this); - private final TabManager tabManager = new TabManager(this::addRenderableWidget, this::removeWidget); - - private @Nullable TabNavigationBar tabNavigationBar; - private @Nullable OptionList optionList; - - public TapestryConfigScreen(Screen parent, Config... configs) { - super(Component.empty()); - assert configs.length > 0; - - this.configs = configs; - this.parent = parent; - } - - @Override - protected void init() { - // Initialise the actual option lists. - ConfigTab[] tabs = new ConfigTab[this.configs.length]; - for (int i = 0; i < this.configs.length; i++) { - OptionList list = createOptionList(this.configs[i], this.width, - this.layout.getContentHeight(), this.layout.getHeaderHeight()); - tabs[i] = new ConfigTab(this.configs[i], list); - } - - if (tabs.length == 1) { - // If we have only a single config, just show a simple title header. - this.layout.addTitleHeader(tabs[0].getTabTitle(), this.font); - this.optionList = this.layout.addToContents(createOptionList(tabs[0].config, this.width, - this.layout.getContentHeight(), this.layout.getHeaderHeight())); - } else { - // If we have more than one config, show them as tabs. - this.tabNavigationBar = this.addRenderableWidget( - TabNavigationBar.builder(this.tabManager, this.width).addTabs(tabs).build()); - this.tabNavigationBar.selectTab(0, false); - } - - // Add the footer cancel and done buttons. - LinearLayout footerLayout = this.layout.addToFooter(LinearLayout.horizontal().spacing(8)); - footerLayout.addChild(Button.builder(CommonComponents.GUI_CANCEL, (button) -> this.onClose()).build()); - footerLayout.addChild(Button.builder(CommonComponents.GUI_DONE, (button) -> this.saveAndClose()).build()); - - this.layout.visitWidgets(this::addRenderableWidget); - this.repositionElements(); - } - - @Override - protected void repositionElements() { - if (this.optionList != null) { - this.layout.arrangeElements(); - this.optionList.updateSize(this.width, this.layout); - } else if (this.tabNavigationBar != null) { - this.tabNavigationBar.setWidth(this.width); - this.tabNavigationBar.arrangeElements(); - - int headerHeight = this.tabNavigationBar.getRectangle().bottom(); - ScreenRectangle screenRectangle = new ScreenRectangle(0, headerHeight, - this.width, this.height - headerHeight - this.layout.getFooterHeight()); - this.tabManager.setTabArea(screenRectangle); - this.layout.setHeaderHeight(headerHeight); - this.layout.arrangeElements(); - } - } - - private OptionList createOptionList(Config config, int width, int height, int y) { - OptionList list = new OptionList(this.minecraft, this.configs.length <= 1, width, height, y); - for (Option option : config.getOptions()) { - if (option.header != null) - list.addHeader(option.header); - - if (!option.hidden) - this.proxies.add(list.addOption(option)); - } - - return list; - } - - public void saveAndClose() { - this.proxies.forEach(OptionProxy::apply); - for (Config config : this.configs) - config.save(); - - this.onClose(); - } - - public void onClose() { - this.minecraft.setScreen(this.parent); - } - - public record ConfigTab(Config config, OptionList list) implements Tab { - @Override - public Component getTabTitle() { - return this.config.getTitle(); - } - - @Override - public Component getTabExtraNarration() { - return Component.empty(); - } - - @Override - public void visitChildren(Consumer consumer) { - consumer.accept(this.list); - } - - @Override - public void doLayout(ScreenRectangle rectangle) { - this.list.updateSizeAndPosition(rectangle.width(), rectangle.height(), rectangle.top()); - } - } -} - diff --git a/src/main/java/me/chrr/tapestry/config/gui/widget/BooleanOptionWidget.java b/src/main/java/me/chrr/tapestry/config/gui/widget/BooleanOptionWidget.java deleted file mode 100644 index 8390e6b..0000000 --- a/src/main/java/me/chrr/tapestry/config/gui/widget/BooleanOptionWidget.java +++ /dev/null @@ -1,40 +0,0 @@ -package me.chrr.tapestry.config.gui.widget; - -import me.chrr.tapestry.config.gui.OptionProxy; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.input.InputWithModifiers; -import net.minecraft.util.ARGB; -import net.minecraft.util.CommonColors; -import org.jspecify.annotations.NullMarked; - -@NullMarked -public class BooleanOptionWidget extends OptionWidget.Clickable { - public BooleanOptionWidget(OptionProxy optionProxy) { - super(optionProxy); - } - - @Override - public void onPress(InputWithModifiers input) { - optionProxy.value = !optionProxy.value; - } - - @Override - protected void renderOptionWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { - int boxSize = this.getHeight() - (2 + 4) * 2; - int boxX = this.getRight() - 2 - 4 - boxSize; - int boxY = this.getY() + 2 + 4; - - int color = CommonColors.WHITE; - int shadow = ARGB.scaleRGB(color, 0.25F); - - this.renderOptionLabel(graphics, boxX - this.getX()); - - graphics.renderOutline(boxX + 1, boxY + 1, boxSize, boxSize, shadow); - graphics.renderOutline(boxX, boxY, boxSize, boxSize, color); - - if (optionProxy.value) { - graphics.fill(boxX + 3, boxY + 3, boxX + boxSize - 1, boxY + boxSize - 1, shadow); - graphics.fill(boxX + 2, boxY + 2, boxX + boxSize - 2, boxY + boxSize - 2, color); - } - } -} diff --git a/src/main/java/me/chrr/tapestry/config/gui/widget/EnumOptionWidget.java b/src/main/java/me/chrr/tapestry/config/gui/widget/EnumOptionWidget.java deleted file mode 100644 index 8cefaa6..0000000 --- a/src/main/java/me/chrr/tapestry/config/gui/widget/EnumOptionWidget.java +++ /dev/null @@ -1,36 +0,0 @@ -package me.chrr.tapestry.config.gui.widget; - -import me.chrr.tapestry.config.gui.OptionProxy; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.input.InputWithModifiers; -import net.minecraft.network.chat.Component; -import org.jspecify.annotations.NullMarked; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@NullMarked -public class EnumOptionWidget extends OptionWidget.Clickable { - private final List values; - private final Map nameByValue = new HashMap<>(); - - public EnumOptionWidget(OptionProxy optionProxy, List values) { - super(optionProxy); - - this.values = values; - for (T value : values) - this.nameByValue.put(value, optionProxy.option.value.textProvider.apply(value)); - } - - @Override - public void onPress(InputWithModifiers input) { - optionProxy.value = values.get((values.indexOf(optionProxy.value) + 1) % values.size()); - } - - @Override - protected void renderOptionWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { - this.renderOptionLabel(graphics, this.getWidth()); - this.renderValueLabel(graphics, 0, this.getWidth(), nameByValue.get(optionProxy.value)); - } -} diff --git a/src/main/java/me/chrr/tapestry/config/gui/widget/OptionWidget.java b/src/main/java/me/chrr/tapestry/config/gui/widget/OptionWidget.java deleted file mode 100644 index 8382eab..0000000 --- a/src/main/java/me/chrr/tapestry/config/gui/widget/OptionWidget.java +++ /dev/null @@ -1,105 +0,0 @@ -package me.chrr.tapestry.config.gui.widget; - -import me.chrr.tapestry.config.gui.OptionProxy; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.ActiveTextCollector; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.components.AbstractWidget; -import net.minecraft.client.gui.components.WidgetSprites; -import net.minecraft.client.gui.components.WidgetTooltipHolder; -import net.minecraft.client.gui.narration.NarrationElementOutput; -import net.minecraft.client.input.InputWithModifiers; -import net.minecraft.client.input.KeyEvent; -import net.minecraft.client.input.MouseButtonEvent; -import net.minecraft.client.renderer.RenderPipelines; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.Identifier; -import net.minecraft.util.ARGB; -import org.jspecify.annotations.NullMarked; - -@NullMarked -public abstract class OptionWidget extends AbstractWidget { - private static final WidgetSprites SPRITES = new WidgetSprites( - Identifier.withDefaultNamespace("widget/button"), - Identifier.withDefaultNamespace("widget/button_disabled"), - Identifier.withDefaultNamespace("widget/button_highlighted") - ); - - public final OptionProxy optionProxy; - public final WidgetTooltipHolder customTooltip; - - public OptionWidget(OptionProxy optionProxy) { - super(0, 0, 0, 0, optionProxy.option.displayName); - - this.optionProxy = optionProxy; - this.customTooltip = new WidgetTooltipHolder(); - } - - @Override - protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { - this.defaultButtonNarrationText(narrationElementOutput); - } - - protected void renderOptionLabel(GuiGraphics graphics, int availableWidth) { - ActiveTextCollector textCollector = graphics.textRendererForWidget(this, GuiGraphics.HoveredTextEffects.NONE); - textCollector.acceptScrolling(this.getMessage(), - this.getX() + 2 + 4, this.getX() + 2 + 4, this.getX() + availableWidth - 2 - 4, - this.getY() + 2, this.getBottom() - 2); - } - - protected void renderValueLabel(GuiGraphics graphics, int rightOffset, int availableWidth) { - this.renderValueLabel(graphics, rightOffset, availableWidth, optionProxy.option.value.textProvider.apply(optionProxy.value)); - } - - protected void renderValueLabel(GuiGraphics graphics, int rightOffset, int availableWidth, Component value) { - ActiveTextCollector textCollector = graphics.textRendererForWidget(this, GuiGraphics.HoveredTextEffects.NONE); - textCollector.acceptScrolling(value, - this.getRight() - rightOffset - 2 - 4, - this.getRight() - rightOffset - availableWidth + 2 + 4, - this.getRight() - rightOffset - 2 - 4, - this.getY() + 2, this.getBottom() - 2); - } - - @Override - protected void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { - graphics.blitSprite( - RenderPipelines.GUI_TEXTURED, SPRITES.get(this.active, this.isHoveredOrFocused()), - this.getX(), this.getY(), this.getWidth(), this.getHeight(), - ARGB.white(this.alpha)); - this.renderOptionWidget(graphics, mouseX, mouseY, partialTick); - } - - protected abstract void renderOptionWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta); - - public static abstract class Clickable extends OptionWidget { - public Clickable(OptionProxy optionProxy) { - super(optionProxy); - } - - @Override - public void onClick(MouseButtonEvent event, boolean isDoubleClick) { - this.onPress(event); - } - - @Override - public boolean keyPressed(KeyEvent event) { - if (!this.isActive()) { - return false; - } else if (event.isSelection()) { - this.playDownSound(Minecraft.getInstance().getSoundManager()); - this.onPress(event); - return true; - } else { - return false; - } - } - - @Override - protected void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { - super.renderWidget(graphics, mouseX, mouseY, partialTick); - this.handleCursor(graphics); - } - - public abstract void onPress(InputWithModifiers input); - } -} diff --git a/src/main/java/me/chrr/tapestry/config/gui/widget/ReadOnlyOptionWidget.java b/src/main/java/me/chrr/tapestry/config/gui/widget/ReadOnlyOptionWidget.java deleted file mode 100644 index dc332a2..0000000 --- a/src/main/java/me/chrr/tapestry/config/gui/widget/ReadOnlyOptionWidget.java +++ /dev/null @@ -1,20 +0,0 @@ -package me.chrr.tapestry.config.gui.widget; - -import me.chrr.tapestry.config.gui.OptionProxy; -import net.minecraft.client.gui.GuiGraphics; -import org.jspecify.annotations.NullMarked; - -@NullMarked -public class ReadOnlyOptionWidget extends OptionWidget { - public ReadOnlyOptionWidget(OptionProxy optionProxy) { - super(optionProxy); - this.active = false; - } - - @Override - protected void renderOptionWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { - this.renderOptionLabel(graphics, this.getWidth()); - this.renderValueLabel(graphics, 0, this.getWidth()); - this.handleCursor(graphics); - } -} diff --git a/src/main/java/me/chrr/tapestry/config/gui/widget/SliderOptionWidget.java b/src/main/java/me/chrr/tapestry/config/gui/widget/SliderOptionWidget.java deleted file mode 100644 index 66c7651..0000000 --- a/src/main/java/me/chrr/tapestry/config/gui/widget/SliderOptionWidget.java +++ /dev/null @@ -1,160 +0,0 @@ -package me.chrr.tapestry.config.gui.widget; - -import com.mojang.blaze3d.platform.InputConstants; -import com.mojang.blaze3d.platform.cursor.CursorTypes; -import me.chrr.tapestry.config.gui.OptionProxy; -import me.chrr.tapestry.config.value.Constraint; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.input.KeyEvent; -import net.minecraft.client.input.MouseButtonEvent; -import net.minecraft.util.ARGB; -import net.minecraft.util.CommonColors; -import org.jspecify.annotations.NullMarked; - -import java.util.Objects; - -@NullMarked -public abstract class SliderOptionWidget extends OptionWidget { - private static final int SLIDER_WIDTH = 96; - - private int sliderMinX = 0; - private int sliderMaxX = 0; - private boolean isSliding = false; - - public final N min; - public final N max; - public final N step; - - public SliderOptionWidget(OptionProxy optionProxy, Constraint.Range range) { - super(optionProxy); - - this.min = range.min(); - this.max = range.max(); - this.step = Objects.requireNonNull(range.step()); - } - - @Override - public boolean keyPressed(KeyEvent event) { - if (this.isFocused()) { - switch (event.key()) { - case InputConstants.KEY_LEFT -> { - optionProxy.value = incrementBySteps(optionProxy.value, -1); - return true; - } - case InputConstants.KEY_RIGHT -> { - optionProxy.value = incrementBySteps(optionProxy.value, 1); - return true; - } - } - } - - return false; - } - - @Override - public void onClick(MouseButtonEvent event, boolean isDoubleClick) { - this.isSliding = event.button() == InputConstants.MOUSE_BUTTON_LEFT - && event.x() >= sliderMinX && event.x() <= sliderMaxX; - this.updateSlider(event); - } - - @Override - public void onRelease(MouseButtonEvent event) { - this.isSliding = false; - } - - @Override - protected void onDrag(MouseButtonEvent event, double mouseX, double mouseY) { - this.updateSlider(event); - } - - protected void updateSlider(MouseButtonEvent event) { - if (!isSliding || event.button() != InputConstants.MOUSE_BUTTON_LEFT) - return; - - float progress = Math.clamp(((float) event.x() - sliderMinX) / SLIDER_WIDTH, 0f, 1f); - optionProxy.value = getValueFromProgress(progress); - } - - @Override - protected void renderOptionWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { - int sliderWidth = this.isHoveredOrFocused() ? SLIDER_WIDTH : SLIDER_WIDTH / 4; - int availableWidth = this.getWidth() - sliderWidth - 4; - this.renderOptionLabel(graphics, availableWidth); - this.renderValueLabel(graphics, sliderWidth + 4, availableWidth); - - int color = CommonColors.WHITE; - int shadow = ARGB.scaleRGB(color, 0.25F); - - sliderMaxX = this.getRight() - 2 - 4; - sliderMinX = sliderMaxX - sliderWidth; - int sliderY = this.getY() + this.getHeight() / 2; - - int thumbWidth = 2; - int thumbHeight = this.getHeight() - (2 + 4) * 2; - float progress = Math.clamp(getProgressFromValue(optionProxy.value), 0f, 1f); - int thumbX = sliderMinX + (int) (sliderWidth * progress) - thumbWidth / 2; - int thumbY = this.getY() + 2 + 4; - - graphics.fill(sliderMinX + 1, sliderY + 1, sliderMaxX + 1, sliderY + 2, shadow); - graphics.fill(sliderMinX, sliderY, sliderMaxX, sliderY + 1, color); - - graphics.fill(thumbX + 1, thumbY + 1, thumbX + thumbWidth + 1, thumbY + thumbHeight + 1, shadow); - graphics.fill(thumbX, thumbY, thumbX + thumbWidth, thumbY + thumbHeight, color); - - if (this.isSliding) { - graphics.requestCursor(CursorTypes.RESIZE_EW); - } else if (this.isHovered() && mouseX >= sliderMinX && mouseX <= sliderMaxX) { - graphics.requestCursor(this.isActive() ? CursorTypes.POINTING_HAND : CursorTypes.NOT_ALLOWED); - } - } - - protected abstract float getProgressFromValue(N value); - - protected abstract N getValueFromProgress(float progress); - - protected abstract N incrementBySteps(N value, int steps); - - - public static class Int extends SliderOptionWidget { - public Int(OptionProxy optionProxy, Constraint.Range range) { - super(optionProxy, range); - } - - @Override - protected float getProgressFromValue(Integer value) { - return ((float) value - this.min) / ((float) this.max - this.min); - } - - @Override - protected Integer getValueFromProgress(float progress) { - return (int) (this.min + progress * (this.max - this.min)) / step * step; - } - - @Override - protected Integer incrementBySteps(Integer value, int steps) { - return Math.clamp(value + (long) this.step * steps, this.min, this.max); - } - } - - public static class Float extends SliderOptionWidget { - public Float(OptionProxy optionProxy, Constraint.Range range) { - super(optionProxy, range); - } - - @Override - protected float getProgressFromValue(java.lang.Float value) { - return (value - this.min) / (this.max - this.min); - } - - @Override - protected java.lang.Float getValueFromProgress(float progress) { - return Math.round((this.min + progress * (this.max - this.min)) / step) * step; - } - - @Override - protected java.lang.Float incrementBySteps(java.lang.Float value, int steps) { - return Math.clamp(value + this.step * steps, this.min, this.max); - } - } -} diff --git a/src/main/java/me/chrr/tapestry/config/value/Constraint.java b/src/main/java/me/chrr/tapestry/config/value/Constraint.java deleted file mode 100644 index cee4be1..0000000 --- a/src/main/java/me/chrr/tapestry/config/value/Constraint.java +++ /dev/null @@ -1,15 +0,0 @@ -package me.chrr.tapestry.config.value; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -import java.util.List; - -@NullMarked -public sealed interface Constraint { - record Range(T min, T max, @Nullable T step) implements Constraint { - } - - record Values(List values) implements Constraint { - } -} diff --git a/src/main/java/me/chrr/tapestry/config/value/TrackedValue.java b/src/main/java/me/chrr/tapestry/config/value/TrackedValue.java deleted file mode 100644 index 662222d..0000000 --- a/src/main/java/me/chrr/tapestry/config/value/TrackedValue.java +++ /dev/null @@ -1,36 +0,0 @@ -package me.chrr.tapestry.config.value; - -import org.jspecify.annotations.NullMarked; - -@NullMarked -public class TrackedValue extends Value { - private final Class type; - private final T defaultValue; - public T value; - - public TrackedValue(Class type, T defaultValue) { - this.type = type; - this.defaultValue = defaultValue; - this.value = defaultValue; - } - - @Override - public void set(T value) { - this.value = value; - } - - @Override - public T getDefaultValue() { - return this.defaultValue; - } - - @Override - public T get() { - return this.value; - } - - @Override - public Class getValueType() { - return this.type; - } -} diff --git a/src/main/java/me/chrr/tapestry/config/value/Value.java b/src/main/java/me/chrr/tapestry/config/value/Value.java deleted file mode 100644 index 8ae5d40..0000000 --- a/src/main/java/me/chrr/tapestry/config/value/Value.java +++ /dev/null @@ -1,38 +0,0 @@ -package me.chrr.tapestry.config.value; - -import net.minecraft.network.chat.Component; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -import java.util.function.Function; -import java.util.function.Supplier; - -@NullMarked -public abstract class Value implements Supplier { - public Function textProvider = (v) -> Component.literal(v.toString()); - public @Nullable Constraint constraint; - - public boolean didSetTextProvider = false; - - public Value range(T min, T max) { - this.constraint = new Constraint.Range<>(min, max, null); - return this; - } - - public Value range(T min, T max, T step) { - this.constraint = new Constraint.Range<>(min, max, step); - return this; - } - - public Value textProvider(Function f) { - this.didSetTextProvider = true; - this.textProvider = f; - return this; - } - - public abstract void set(T value); - - public abstract T getDefaultValue(); - - public abstract Class getValueType(); -} diff --git a/src/main/java/me/chrr/tapestry/config/value/VirtualValue.java b/src/main/java/me/chrr/tapestry/config/value/VirtualValue.java deleted file mode 100644 index f25fb64..0000000 --- a/src/main/java/me/chrr/tapestry/config/value/VirtualValue.java +++ /dev/null @@ -1,41 +0,0 @@ -package me.chrr.tapestry.config.value; - -import org.jspecify.annotations.NullMarked; - -import java.util.function.Consumer; -import java.util.function.Supplier; - -@NullMarked -public class VirtualValue extends Value { - private final Class type; - private final T defaultValue; - private final Supplier getter; - private final Consumer setter; - - public VirtualValue(Class type, T defaultValue, Supplier getter, Consumer setter) { - this.type = type; - this.defaultValue = defaultValue; - this.getter = getter; - this.setter = setter; - } - - @Override - public void set(T value) { - this.setter.accept(value); - } - - @Override - public T getDefaultValue() { - return this.defaultValue; - } - - @Override - public T get() { - return this.getter.get(); - } - - @Override - public Class getValueType() { - return this.type; - } -} diff --git a/src/main/resources/assets/tapestry-config/textures/gui/reset.png b/src/main/resources/assets/tapestry-config/textures/gui/reset.png deleted file mode 100644 index bec6b7f86a4e2bd3bef62f69638350f155ed22d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!D3?x-;bCrM;V{wqX6XVU3I`u#fXMsm#F#`j) zFbFd;%$g$s6l4qV32_C||NsAAdUO6=Ad9Ia$S;`TfAmD9tw5flr;B4q#hm1ngajA2 tHM=EmE|IutDJjX~(BauPvCw`K6N64I6Qc Date: Sun, 10 May 2026 16:52:31 +0200 Subject: [PATCH 9/9] Update ScribbleBookEditScreen.java --- .../chrr/scribble/screen/ScribbleBookEditScreen.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java b/common/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java index 881de47..ef22749 100644 --- a/common/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java +++ b/common/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java @@ -250,14 +250,16 @@ private OverflowHandler createOverflowHandlerForPage(int pageOffset) { @Override public boolean handleOverflow(RichText currentText, int cursor, RichText insert, @Nullable ChatFormatting color, Set modifiers) { - if (overflowHandler == null || getTotalPages() >= 100) return false; + if (overflowHandler == null || getTotalPages() >= 100) + return false; int page = currentPage + pageOffset; return overflowHandler.insertWithOverflow(page, currentText, cursor, insert, color, modifiers); } @Override public boolean handleEnterAtEnd() { - if (getTotalPages() >= 100) return false; + if (getTotalPages() >= 100) + return false; int page = currentPage + pageOffset; PageInsertCommand command = new PageInsertCommand(page + 1); command.execute(ScribbleBookEditScreen.this); @@ -267,9 +269,11 @@ public boolean handleEnterAtEnd() { @Override public boolean handleBackspaceOnEmpty() { - if (getTotalPages() <= 1) return false; + if (getTotalPages() <= 1) + return false; int page = currentPage + pageOffset; - if (page <= 0) return false; + if (page <= 0) + return false; PageDeleteCommand command = new PageDeleteCommand(page, pages.get(page), -1); // Navigate left command.execute(ScribbleBookEditScreen.this); pushCommand(command);