From 037c51e40411fe02a631ce1d9193c4c619feafd3 Mon Sep 17 00:00:00 2001 From: chrrrs Date: Sat, 20 Dec 2025 16:28:47 +0100 Subject: [PATCH 01/16] feat: move to book recreation screens + other changes --- .github/ISSUE_TEMPLATE/bug-report.yml | 52 ++ .github/ISSUE_TEMPLATE/feature-request.yml | 20 + .github/pull_request_template.md | 1 + CONTRIBUTING.md | 21 + build.gradle.kts | 2 + fabric/build.gradle.kts | 1 - .../chrr/scribble/fabric/DebugifyCompat.java | 10 - .../chrr/scribble/fabric/ModMenuCompat.java | 12 +- .../chrr/scribble/fabric/ScribbleFabric.java | 30 +- fabric/src/main/resources/fabric.mod.json | 3 - gradle.properties | 1 + .../scribble/neoforge/ScribbleNeoForge.java | 36 +- .../java/me/chrr/scribble/KeyboardUtil.java | 11 +- src/main/java/me/chrr/scribble/Scribble.java | 44 +- .../me/chrr/scribble/SetReturnScreen.java | 7 + .../java/me/chrr/scribble/book/BookFile.java | 2 + .../me/chrr/scribble/book/FileChooser.java | 21 +- .../java/me/chrr/scribble/book/RichText.java | 28 +- .../config/ClothConfigScreenFactory.java | 16 +- .../java/me/chrr/scribble/config/Config.java | 4 + .../chrr/scribble/config/ConfigManager.java | 5 +- .../config/YACLConfigScreenFactory.java | 85 +++ .../me/chrr/scribble/gui/BookTextWidget.java | 137 +++++ .../chrr/scribble/gui/PageNumberWidget.java | 2 + .../java/me/chrr/scribble/gui/TextArea.java | 13 + .../gui/button/ColorSwatchWidget.java | 7 +- .../scribble/gui/button/IconButtonWidget.java | 2 + .../gui/button/ModifierButtonWidget.java | 7 +- ...ichEditBoxWidget.java => RichEditBox.java} | 77 ++- .../gui/edit/RichMultiLineTextField.java | 69 ++- .../chrr/scribble/history/CommandManager.java | 9 +- .../scribble/history/HistoryListener.java | 14 +- .../scribble/history/command/Command.java | 2 + .../scribble/history/command/EditCommand.java | 60 +- .../history/command/PageDeleteCommand.java | 6 +- .../history/command/PageInsertCommand.java | 6 +- .../scribble/mixin/BookEditScreenMixin.java | 549 ------------------ .../scribble/mixin/BookSignScreenMixin.java | 55 +- .../scribble/mixin/BookViewScreenMixin.java | 152 ----- .../mixin/ClientPacketListenerMixin.java | 28 + .../scribble/mixin/KeyboardHandlerMixin.java | 9 +- .../scribble/mixin/LecternScreenMixin.java | 31 - .../chrr/scribble/mixin/LocalPlayerMixin.java | 30 + .../screen/ScribbleBookEditScreen.java | 462 +++++++++++++++ .../scribble/screen/ScribbleBookScreen.java | 233 ++++++++ .../screen/ScribbleBookViewScreen.java | 90 +++ .../resources/assets/scribble/lang/en_us.json | 1 + .../assets/scribble/textures/gui/book_2.png | Bin 0 -> 11053 bytes .../textures/gui/scribble_widgets.png | Bin 5324 -> 1944 bytes src/main/resources/scribble.mixins.json | 5 +- 50 files changed, 1516 insertions(+), 952 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml create mode 100644 .github/pull_request_template.md create mode 100644 CONTRIBUTING.md delete mode 100644 fabric/src/main/java/me/chrr/scribble/fabric/DebugifyCompat.java create mode 100644 src/main/java/me/chrr/scribble/SetReturnScreen.java create mode 100644 src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java create mode 100644 src/main/java/me/chrr/scribble/gui/BookTextWidget.java create mode 100644 src/main/java/me/chrr/scribble/gui/TextArea.java rename src/main/java/me/chrr/scribble/gui/edit/{RichEditBoxWidget.java => RichEditBox.java} (80%) delete mode 100644 src/main/java/me/chrr/scribble/mixin/BookEditScreenMixin.java delete mode 100644 src/main/java/me/chrr/scribble/mixin/BookViewScreenMixin.java create mode 100644 src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java delete mode 100644 src/main/java/me/chrr/scribble/mixin/LecternScreenMixin.java create mode 100644 src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java create mode 100644 src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java create mode 100644 src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java create mode 100644 src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java create mode 100644 src/main/resources/assets/scribble/textures/gui/book_2.png diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..8a80fb1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,52 @@ +name: Bug Report +description: Report a bug with the mod. +labels: [ "bug" ] + +body: + - type: markdown + attributes: + value: | + Before opening an issue, please briefly look through the [open issues](https://github.com/chrrs/scribble/issues) + to see if a similar bug report already exists. + + - type: textarea + id: description + validations: + required: true + attributes: + label: Describe the issue + description: | + What's the problem? Describe the bug, what doesn't work, and what did you expect to happen? If you have any log + files, make sure to include them. If a screenshot would help to describe the problem, please include that too! + + - type: input + id: minecraftVersion + validations: + required: true + attributes: + label: Minecraft Version + placeholder: ex. "1.20.1" + + - type: input + id: modVersion + validations: + required: true + attributes: + label: Scribble Version + description: | + If you're not sure, it will show up by default if the feather icon is hovered in the bottom left of the book + editing screen. Alternatively, the version can be found in a mod menu, or the file name of the downloaded mod. + placeholder: ex. "2.3.4" + + - type: dropdown + id: loader + validations: + required: true + attributes: + label: Mod Loader + description: | + If the issue applies to multiple loaders, please select the loader that it was encountered on. + options: + - Fabric / Quilt + - NeoForge + - Forge diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..6e6ad65 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,20 @@ +name: Feature Request +description: Propose a new feature or a change to the mod. +labels: [ "enhancement" ] + +body: + - type: markdown + attributes: + value: | + Before opening an issue, please briefly look through the [open issues](https://github.com/chrrs/scribble/issues) + to see if a similar request already exists. + + - type: textarea + id: description + validations: + required: true + attributes: + label: Describe the feature / change + description: | + Describe what the new feature or change would be. Please be specific, and motivate your answer, it helps make + requests more understandable! diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..4fdf76a --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4502498 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,21 @@ +Hi! Thanks for considering contributing to Scribble. Here's some things to keep in mind. For the most part, these are +not hard rules, but suggestions to keep the code manageable. + +### LLMs and AI + +**Any contributions generated with AI or assisted by AI will not be accepted!** This is inflexible, it's a personal +preference, please respect it. + +### Code style + +There's not a set list of guidelines to follow here, but try to match the code style to the rest of the code base. This +includes: descriptive variable names, use plenty of whitespace to group related lines together and don't be afraid to +separate out some variables if lines grow too long. + +Make sure to use IntelliJ's formatter and import organizer to manage indentation and spacing. + +### Reviews + +I generally make a lot of comments during PR reviews, please don't take this personally! My end goal is to try and keep +the code base clean and maintainable, as both Minecraft's code base changes in unpredictable ways, and Scribble keeps +growing in features. Keeping the code clean and properly separated helps with this a lot! diff --git a/build.gradle.kts b/build.gradle.kts index 2d5a5c6..bb4df55 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,6 +22,7 @@ architectury.common(stonecutter.tree.branches.mapNotNull { repositories { maven("https://maven.shedaniel.me/") + maven("https://maven.isxander.dev/releases") } dependencies { @@ -30,6 +31,7 @@ dependencies { modImplementation("net.fabricmc:fabric-loader:${prop("fabric", "loaderVersion")}") modCompileOnly("me.shedaniel.cloth:cloth-config-fabric:${prop("clothconfig", "version")}") + modCompileOnly("dev.isxander:yet-another-config-lib:${prop("yacl", "version")}") } loom { diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index 2d91b41..4b5b46d 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -47,7 +47,6 @@ dependencies { include(fabricApiModule("fabric-resource-loader-v0")!!) modCompileOnly("com.terraformersmc:modmenu:${common.prop("modmenu", "version")}") - modCompileOnly("dev.isxander:debugify:1.21.8+1.0") { isTransitive = false } commonBundle(project(common.path, "namedElements")) { isTransitive = false } shadowBundle(project(common.path, "transformProductionFabric")) { isTransitive = false } diff --git a/fabric/src/main/java/me/chrr/scribble/fabric/DebugifyCompat.java b/fabric/src/main/java/me/chrr/scribble/fabric/DebugifyCompat.java deleted file mode 100644 index 4c46a10..0000000 --- a/fabric/src/main/java/me/chrr/scribble/fabric/DebugifyCompat.java +++ /dev/null @@ -1,10 +0,0 @@ -package me.chrr.scribble.fabric; - -import dev.isxander.debugify.api.DebugifyApi; - -public class DebugifyCompat implements DebugifyApi { - @Override - public String[] getDisabledFixes() { - return new String[]{"MC-61489"}; - } -} 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 7d8c928..10c0e77 100644 --- a/fabric/src/main/java/me/chrr/scribble/fabric/ModMenuCompat.java +++ b/fabric/src/main/java/me/chrr/scribble/fabric/ModMenuCompat.java @@ -2,14 +2,16 @@ import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; -import me.chrr.scribble.config.ClothConfigScreenFactory; -import net.fabricmc.loader.api.FabricLoader; +import me.chrr.scribble.Scribble; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +@NullMarked public class ModMenuCompat implements ModMenuApi { @Override - public ConfigScreenFactory getModConfigScreenFactory() { - if (FabricLoader.getInstance().isModLoaded("cloth-config2")) { - return ClothConfigScreenFactory::create; + public @Nullable ConfigScreenFactory getModConfigScreenFactory() { + if (Scribble.platform().HAS_YACL) { + return Scribble::buildConfigScreen; } return null; 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 b7ad442..9df2051 100644 --- a/fabric/src/main/java/me/chrr/scribble/fabric/ScribbleFabric.java +++ b/fabric/src/main/java/me/chrr/scribble/fabric/ScribbleFabric.java @@ -3,13 +3,35 @@ import me.chrr.scribble.Scribble; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.loader.api.FabricLoader; +import org.jspecify.annotations.NullMarked; -public class ScribbleFabric implements ClientModInitializer { +import java.nio.file.Path; + +@NullMarked +public class ScribbleFabric extends Scribble.Platform implements ClientModInitializer { @Override public void onInitializeClient() { - Scribble.CONFIG_DIR = FabricLoader.getInstance().getConfigDir(); - Scribble.BOOK_DIR = FabricLoader.getInstance().getGameDir().resolve("books"); + Scribble.init(this); + } + + @Override + protected boolean isModLoaded(String modId) { + return FabricLoader.getInstance().isModLoaded(modId); + } + + @Override + protected String getModVersion() { + return FabricLoader.getInstance().getModContainer(Scribble.MOD_ID) + .orElseThrow().getMetadata().getVersion().getFriendlyString(); + } + + @Override + protected Path getConfigDir() { + return FabricLoader.getInstance().getConfigDir(); + } - Scribble.init(); + @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 index 1f70833..af3d3df 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -27,9 +27,6 @@ ], "modmenu": [ "me.chrr.scribble.fabric.ModMenuCompat" - ], - "debugify": [ - "me.chrr.scribble.fabric.DebugifyCompat" ] }, "depends": { diff --git a/gradle.properties b/gradle.properties index 4691d4d..a5d7372 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,6 +22,7 @@ neoforge.version=[versioned] # Dependencies modmenu.version=11.0.2 clothconfig.version=15.0.140 +yacl.version=3.8.1+1.21.11-fabric # Distribution modrinth.id=yXAvIk0x 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 7430c14..a994ac4 100644 --- a/neoforge/src/main/java/me/chrr/scribble/neoforge/ScribbleNeoForge.java +++ b/neoforge/src/main/java/me/chrr/scribble/neoforge/ScribbleNeoForge.java @@ -1,25 +1,45 @@ package me.chrr.scribble.neoforge; import me.chrr.scribble.Scribble; -import me.chrr.scribble.config.ClothConfigScreenFactory; 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.CONFIG_DIR = FMLPaths.CONFIGDIR.get(); - Scribble.BOOK_DIR = FMLPaths.GAMEDIR.get().resolve("books"); - - Scribble.init(); + Scribble.init(this); - if (ModList.get().isLoaded("cloth_config")) { + if (Scribble.platform().HAS_YACL) { mod.registerExtensionPoint(IConfigScreenFactory.class, - (container, parent) -> ClothConfigScreenFactory.create(parent)); + (container, parent) -> Scribble.buildConfigScreen(parent)); } } + + @Override + protected boolean isModLoaded(String modId) { + return ModList.get().isLoaded(modId); + } + + @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/src/main/java/me/chrr/scribble/KeyboardUtil.java b/src/main/java/me/chrr/scribble/KeyboardUtil.java index 2e59635..595c23c 100644 --- a/src/main/java/me/chrr/scribble/KeyboardUtil.java +++ b/src/main/java/me/chrr/scribble/KeyboardUtil.java @@ -1,10 +1,9 @@ package me.chrr.scribble; -import com.mojang.blaze3d.platform.InputConstants; -import com.mojang.blaze3d.platform.Window; -import net.minecraft.client.Minecraft; +import org.jspecify.annotations.NullMarked; import org.lwjgl.glfw.GLFW; +@NullMarked public class KeyboardUtil { private KeyboardUtil() { } @@ -20,10 +19,4 @@ public static boolean isKey(int keyCode, String keyName) { return keyName.equalsIgnoreCase(GLFW.glfwGetKeyName(keyCode, 0)); } } - - public static boolean hasShiftDown() { - Window window = Minecraft.getInstance().getWindow(); - return InputConstants.isKeyDown(window, GLFW.GLFW_KEY_LEFT_SHIFT) - || InputConstants.isKeyDown(window, GLFW.GLFW_KEY_RIGHT_SHIFT); - } } diff --git a/src/main/java/me/chrr/scribble/Scribble.java b/src/main/java/me/chrr/scribble/Scribble.java index f2fc22b..654221a 100644 --- a/src/main/java/me/chrr/scribble/Scribble.java +++ b/src/main/java/me/chrr/scribble/Scribble.java @@ -1,24 +1,31 @@ package me.chrr.scribble; import me.chrr.scribble.book.FileChooser; +import me.chrr.scribble.config.Config; import me.chrr.scribble.config.ConfigManager; +import me.chrr.scribble.config.YACLConfigScreenFactory; +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.io.IOException; import java.nio.file.Path; +import java.util.Optional; +@NullMarked public class Scribble { public static final String MOD_ID = "scribble"; public static final Logger LOGGER = LogManager.getLogger(); - public static final ConfigManager CONFIG_MANAGER = new ConfigManager(); + private static final ConfigManager CONFIG_MANAGER = new ConfigManager(); + private static @Nullable Platform PLATFORM; - public static Path CONFIG_DIR = null; - public static Path BOOK_DIR = null; + public static void init(Platform platform) { + PLATFORM = platform; - public static void init() { try { CONFIG_MANAGER.load(); } catch (IOException e) { @@ -32,11 +39,36 @@ public static void init() { } } - public static int getBookScreenYOffset(int screenHeight) { - return Scribble.CONFIG_MANAGER.getConfig().centerBookGui ? (screenHeight - 192) / 3 : 0; + public static Screen buildConfigScreen(Screen parent) { + return YACLConfigScreenFactory.create(CONFIG_MANAGER, parent); + } + + public static Platform platform() { + return Optional.ofNullable(PLATFORM).orElseThrow(); + } + + public static Config config() { + return CONFIG_MANAGER.getConfig(); } 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(); + public final boolean HAS_CLOTH_CONFIG = isModLoaded("cloth_config") || isModLoaded("cloth-config"); + public final boolean HAS_YACL = isModLoaded("yet_another_config_lib_v3"); + + protected abstract boolean isModLoaded(String modId); + + protected abstract String getModVersion(); + + protected abstract Path getConfigDir(); + + protected abstract Path getGameDir(); + } } diff --git a/src/main/java/me/chrr/scribble/SetReturnScreen.java b/src/main/java/me/chrr/scribble/SetReturnScreen.java new file mode 100644 index 0000000..70551fb --- /dev/null +++ b/src/main/java/me/chrr/scribble/SetReturnScreen.java @@ -0,0 +1,7 @@ +package me.chrr.scribble; + +import net.minecraft.client.gui.screens.Screen; + +public interface SetReturnScreen { + void scribble$setReturnScreen(Screen screen); +} diff --git a/src/main/java/me/chrr/scribble/book/BookFile.java b/src/main/java/me/chrr/scribble/book/BookFile.java index 0aada61..07e61e0 100644 --- a/src/main/java/me/chrr/scribble/book/BookFile.java +++ b/src/main/java/me/chrr/scribble/book/BookFile.java @@ -6,6 +6,7 @@ import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NbtIo; import net.minecraft.nbt.Tag; +import org.jspecify.annotations.NullMarked; import java.io.IOException; import java.nio.file.Files; @@ -23,6 +24,7 @@ * @param author the author of the book file. Usually equal to a username. * @param pages the rich-text pages of the book. */ +@NullMarked public record BookFile(String author, Collection pages) { private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); diff --git a/src/main/java/me/chrr/scribble/book/FileChooser.java b/src/main/java/me/chrr/scribble/book/FileChooser.java index 6c1f067..b8939b6 100644 --- a/src/main/java/me/chrr/scribble/book/FileChooser.java +++ b/src/main/java/me/chrr/scribble/book/FileChooser.java @@ -3,7 +3,7 @@ import me.chrr.scribble.Scribble; import net.minecraft.client.Minecraft; import net.minecraft.locale.Language; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NullMarked; import org.lwjgl.PointerBuffer; import org.lwjgl.system.MemoryStack; import org.lwjgl.system.Platform; @@ -15,6 +15,7 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.function.Consumer; +@NullMarked public class FileChooser { private FileChooser() { } @@ -25,7 +26,7 @@ private FileChooser() { * @param save if the dialog should be a save dialog instead of an open dialog. * @param pathConsumer the callback to call when a path is successfully chosen. */ - public static void chooseBook(boolean save, Consumer pathConsumer) { + public static void chooseFile(boolean save, Consumer pathConsumer) { new Thread(() -> { try (MemoryStack stack = MemoryStack.stackPush()) { PointerBuffer filter = null; @@ -80,15 +81,17 @@ public static void chooseBook(boolean save, Consumer pathConsumer) { * @return the book directory. */ private static Path createAndGetBookDirectory() { + Path bookDir = Scribble.platform().BOOK_DIR; + try { - if (!Files.exists(Scribble.BOOK_DIR)) { - Files.createDirectory(Scribble.BOOK_DIR); + if (!Files.exists(bookDir)) { + Files.createDirectory(bookDir); } } catch (Exception ignored) { Scribble.LOGGER.warn("couldn't create the default book directory"); } - return Scribble.BOOK_DIR; + return bookDir; } /** @@ -98,7 +101,7 @@ private static Path createAndGetBookDirectory() { * @throws IOException when an error happens. */ public static void convertLegacyBooks() throws IOException { - Path rootDir = Scribble.BOOK_DIR; + Path rootDir = Scribble.platform().BOOK_DIR; Path legacyDir = rootDir.resolve("_legacy"); if (!rootDir.toFile().isDirectory()) { @@ -107,8 +110,7 @@ public static void convertLegacyBooks() throws IOException { Files.walkFileTree(rootDir, new SimpleFileVisitor<>() { @Override - @NotNull - public FileVisitResult preVisitDirectory(@NotNull Path dir, @NotNull BasicFileAttributes attrs) { + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { if (dir.getFileName().toString().equals("_legacy")) { return FileVisitResult.SKIP_SUBTREE; } else { @@ -117,8 +119,7 @@ public FileVisitResult preVisitDirectory(@NotNull Path dir, @NotNull BasicFileAt } @Override - @NotNull - public FileVisitResult visitFile(@NotNull Path file, @NotNull BasicFileAttributes attrs) { + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { if (file.toString().endsWith(".book")) { Scribble.LOGGER.info("converting legacy NBT-based book file at {} to JSON.", file); diff --git a/src/main/java/me/chrr/scribble/book/RichText.java b/src/main/java/me/chrr/scribble/book/RichText.java index 470cbba..7990207 100644 --- a/src/main/java/me/chrr/scribble/book/RichText.java +++ b/src/main/java/me/chrr/scribble/book/RichText.java @@ -1,12 +1,12 @@ package me.chrr.scribble.book; +import com.mojang.datafixers.util.Pair; import com.mojang.serialization.MapCodec; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.*; import net.minecraft.network.chat.contents.PlainTextContents; -import net.minecraft.util.Tuple; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import java.util.*; import java.util.concurrent.atomic.AtomicReference; @@ -27,7 +27,10 @@ * * @author chrrrs */ +@NullMarked public class RichText implements FormattedText { + public static final RichText EMPTY = new RichText(List.of()); + private final List segments; /** @@ -182,7 +185,6 @@ private static ChatFormatting formattingFromTextColor(TextColor color) { * * @return a new {@link RichText} object that has the same contents in potentially fewer segments. */ - @NotNull private RichText mergeSimilarSegments() { if (this.segments.isEmpty()) { return this; @@ -516,19 +518,19 @@ public MutableComponent getAsMutableComponent() { RichText text = this; return MutableComponent.create(new ComponentContents() { @Override - public @NotNull Optional visit(FormattedText.StyledContentConsumer consumer, Style style) { + public Optional visit(FormattedText.StyledContentConsumer consumer, Style style) { return text.visit(consumer, style); } @Override - public @NotNull Optional visit(FormattedText.ContentConsumer consumer) { + public Optional visit(FormattedText.ContentConsumer consumer) { return text.visit(consumer); } // This is not accurate, but these contents are never sent to the // server, so it doesn't need to be. @Override - public @NotNull MapCodec codec() { + public MapCodec codec() { return PlainTextContents.MAP_CODEC; } }); @@ -542,7 +544,7 @@ public MutableComponent getAsMutableComponent() { * @param end end of text range (exclusive). * @return a pair with the common color and modifiers. */ - public Tuple<@Nullable ChatFormatting, Set> getCommonFormat(int start, int end) { + public Pair<@Nullable ChatFormatting, Set> getCommonFormat(int start, int end) { boolean first = true; Set modifiers = Set.of(); ChatFormatting color = ChatFormatting.BLACK; @@ -554,7 +556,7 @@ public MutableComponent getAsMutableComponent() { // If we have a zero-width selection, we want the formatting of // the segment before it. if (start == end && start <= current + length) { - return new Tuple<>(segment.color, segment.modifiers); + return new Pair<>(segment.color, segment.modifiers); } // We're before the segment we're searching for @@ -586,11 +588,11 @@ public MutableComponent getAsMutableComponent() { current += length; } - return new Tuple<>(color, modifiers); + return new Pair<>(color, modifiers); } @Override - public @NotNull Optional visit(ContentConsumer consumer) { + public Optional visit(ContentConsumer consumer) { for (Segment segment : segments) { Optional out = consumer.accept(segment.text); if (out.isPresent()) { @@ -602,7 +604,7 @@ public MutableComponent getAsMutableComponent() { } @Override - public @NotNull Optional visit(StyledContentConsumer consumer, Style baseStyle) { + public Optional visit(StyledContentConsumer consumer, Style baseStyle) { for (Segment segment : segments) { Style style = baseStyle .applyFormats(segment.modifiers.toArray(new ChatFormatting[0])) @@ -617,7 +619,7 @@ public MutableComponent getAsMutableComponent() { } @Override - public @NotNull String getString() { + public String getString() { return this.getPlainText(); } diff --git a/src/main/java/me/chrr/scribble/config/ClothConfigScreenFactory.java b/src/main/java/me/chrr/scribble/config/ClothConfigScreenFactory.java index fbc5131..6fef0c9 100644 --- a/src/main/java/me/chrr/scribble/config/ClothConfigScreenFactory.java +++ b/src/main/java/me/chrr/scribble/config/ClothConfigScreenFactory.java @@ -6,27 +6,29 @@ import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; +import org.jspecify.annotations.NullMarked; import java.io.IOException; +@NullMarked public class ClothConfigScreenFactory { private ClothConfigScreenFactory() { } - public static Screen create(Screen parent) { + public static Screen create(ConfigManager configManager, Screen parent) { ConfigBuilder builder = ConfigBuilder.create() .setParentScreen(parent) .setTitle(Component.translatable("config.scribble.title")); builder.setSavingRunnable(() -> { try { - Scribble.CONFIG_MANAGER.save(); + configManager.save(); } catch (IOException e) { Scribble.LOGGER.error("could not save config", e); } }); - Config config = Scribble.CONFIG_MANAGER.getConfig(); + Config config = configManager.getConfig(); ConfigCategory category = builder.getOrCreateCategory(Component.empty()); ConfigEntryBuilder entryBuilder = builder.entryBuilder(); @@ -66,6 +68,14 @@ public static Screen create(Screen parent) { .setSaveConsumer((value) -> config.editHistorySize = value) .build()); + category.addEntry(entryBuilder.startIntField( + Component.literal("Pages to show"), + config.pagesToShow + ) + .setDefaultValue(Config.DEFAULT.pagesToShow) + .setSaveConsumer((value) -> config.pagesToShow = value) + .build()); + return builder.build(); } } diff --git a/src/main/java/me/chrr/scribble/config/Config.java b/src/main/java/me/chrr/scribble/config/Config.java index e71aeab..74404d8 100644 --- a/src/main/java/me/chrr/scribble/config/Config.java +++ b/src/main/java/me/chrr/scribble/config/Config.java @@ -1,5 +1,8 @@ package me.chrr.scribble.config; +import org.jspecify.annotations.NullMarked; + +@NullMarked public class Config { public static final Config DEFAULT = new Config(); @@ -8,6 +11,7 @@ public class Config { public boolean centerBookGui = true; public ShowActionButtons showActionButtons = ShowActionButtons.WHEN_EDITING; public int editHistorySize = 32; + public int pagesToShow = 1; @DeprecatedConfigOption private boolean showSaveLoadButtons = true; diff --git a/src/main/java/me/chrr/scribble/config/ConfigManager.java b/src/main/java/me/chrr/scribble/config/ConfigManager.java index 6744f57..3063aa3 100644 --- a/src/main/java/me/chrr/scribble/config/ConfigManager.java +++ b/src/main/java/me/chrr/scribble/config/ConfigManager.java @@ -2,11 +2,13 @@ import com.google.gson.*; import me.chrr.scribble.Scribble; +import org.jspecify.annotations.NullMarked; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +@NullMarked public class ConfigManager { private static final Gson GSON = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) @@ -35,13 +37,14 @@ public void save() throws IOException { } private Path getConfigPath() { - return Scribble.CONFIG_DIR.resolve("scribble.json"); + return Scribble.platform().CONFIG_DIR.resolve("scribble.json"); } /// Exclusion strategy to skip all fields that are annotated with {@link DeprecatedConfigOption}. private static class SkipDeprecatedStrategy implements ExclusionStrategy { @Override public boolean shouldSkipField(FieldAttributes f) { + //noinspection ConstantValue: this inspection is a false-positive. return f.getAnnotation(DeprecatedConfigOption.class) != null; } diff --git a/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java b/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java new file mode 100644 index 0000000..ea7b2b9 --- /dev/null +++ b/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java @@ -0,0 +1,85 @@ +package me.chrr.scribble.config; + +import dev.isxander.yacl3.api.*; +import dev.isxander.yacl3.api.controller.EnumControllerBuilder; +import dev.isxander.yacl3.api.controller.IntegerSliderControllerBuilder; +import dev.isxander.yacl3.api.controller.TickBoxControllerBuilder; +import me.chrr.scribble.Scribble; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.NullMarked; + +import java.io.IOException; + +@NullMarked +public class YACLConfigScreenFactory { + public static Screen create(ConfigManager configManager, Screen parent) { + Config config = configManager.getConfig(); + + // FIXME: if we want to keep YACL, move text to translatable strings. + return YetAnotherConfigLib.createBuilder() + .title(Component.translatable("config.scribble.title")) + .category(ConfigCategory.createBuilder() + .name(Component.translatable("config.scribble.title")) + .group(OptionGroup.createBuilder() + .name(Component.literal("Appearance")) + .description(OptionDescription.of(Component.literal("These options affect how the user interface looks."))) + .option(Option.createBuilder() + .name(Component.literal("Double page viewing")) + .description(OptionDescription.of(Component.literal("Whether to show two pages at the same time when reading and editing books."))) + .binding(Config.DEFAULT.pagesToShow > 1, () -> config.pagesToShow > 1, + (value) -> config.pagesToShow = value ? 2 : 1) + .controller(TickBoxControllerBuilder::create) + .build()) + .option(Option.createBuilder() + .name(Component.literal("Vertically center book GUI's")) + .description(OptionDescription.of(Component.literal("If enabled, book viewing and editing GUI's will be approximately vertically centered, instead of being fixed to the top of the screen."))) + .binding(Config.DEFAULT.centerBookGui, () -> config.centerBookGui, + (value) -> config.centerBookGui = value) + .controller(TickBoxControllerBuilder::create) + .build()) + .option(Option.createBuilder() + .name(Component.literal("Show action buttons")) + .description(OptionDescription.of(Component.literal("Action buttons are the white buttons on the left of the page. This option determines when these action buttons should be shown or hidden.\n\nThe 'Show when editing' option shows the action buttons when editing a book using a book & quill, but hides them when reading a written book."))) + .binding(Config.DEFAULT.showActionButtons, () -> config.showActionButtons, + (value) -> config.showActionButtons = value) + .controller((opt) -> EnumControllerBuilder.create(opt) + .enumClass(Config.ShowActionButtons.class) + .formatValue((value) -> switch (value) { + case ALWAYS -> Component.literal("Always"); + case WHEN_EDITING -> Component.literal("Show when editing"); + case NEVER -> Component.literal("Never"); + })) + .build()) + .build()) + .group(OptionGroup.createBuilder() + .name(Component.literal("Behaviour")) + .description(OptionDescription.of(Component.literal("These options affect how you can write books."))) + .option(Option.createBuilder() + .name(Component.literal("Copy formatting codes")) + .description(OptionDescription.of(Component.literal("When copying formatted text, this option determines whether formatting codes (&) should be copied to your clipboard. This allows you to paste text back into the book using the copied formatting.\n\nNote: This option can be temporarily disabled by holding SHIFT while copying or pasting text."))) + .binding(Config.DEFAULT.copyFormattingCodes, () -> config.copyFormattingCodes, + (value) -> config.copyFormattingCodes = value) + .controller(TickBoxControllerBuilder::create) + .build()) + .option(Option.createBuilder() + .name(Component.literal("Edit history limit")) + .description(OptionDescription.of(Component.literal("How many actions Scribble should remember for you to be able to undo them. If the limit is exceeded, the oldest actions will be removed from the undo stack.\n\nNote: Higher values could lead to more RAM usage while editing."))) + .binding(Config.DEFAULT.editHistorySize, () -> config.editHistorySize, + (value) -> config.editHistorySize = value) + .controller((opt) -> IntegerSliderControllerBuilder.create(opt) + .range(8, 128).step(1)) + .build()) + .build()) + .build()) + .save(() -> { + try { + configManager.save(); + } catch (IOException e) { + Scribble.LOGGER.error("could not save config", e); + } + }) + .build() + .generateScreen(parent); + } +} diff --git a/src/main/java/me/chrr/scribble/gui/BookTextWidget.java b/src/main/java/me/chrr/scribble/gui/BookTextWidget.java new file mode 100644 index 0000000..85f7386 --- /dev/null +++ b/src/main/java/me/chrr/scribble/gui/BookTextWidget.java @@ -0,0 +1,137 @@ +package me.chrr.scribble.gui; + +import net.minecraft.client.gui.ActiveTextCollector; +import net.minecraft.client.gui.Font; +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; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.network.chat.*; +import net.minecraft.util.FormattedCharSequence; +import org.jspecify.annotations.NullMarked; + +import java.util.List; +import java.util.function.Consumer; + +// FIXME: is there a non-interactable-but-clickable abstract widget class I can use here? +// AbstractStringWidget is a thing, though I'm not sure it does what I need to. +@NullMarked +public class BookTextWidget implements TextArea { + public static final Style PAGE_TEXT_STYLE = Style.EMPTY.withoutShadow().withColor(0xff000000); + + private List lines = List.of(); + private Component text = Component.empty(); + + private boolean visible = true; + private boolean hovered = false; + + private final int x; + private final int y; + private final int width; + private final int height; + + private final Font font; + private final Consumer handleClickEvent; + + public BookTextWidget(int x, int y, int width, int height, Font font, Consumer handleClickEvent) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + + this.font = font; + this.handleClickEvent = handleClickEvent; + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + if (!visible) + return; + + this.hovered = guiGraphics.containsPointInScissor(mouseX, mouseY) + && this.areCoordinatesInRectangle(mouseX, mouseY); + this.visitText(guiGraphics.textRenderer(GuiGraphics.HoveredTextEffects.TOOLTIP_AND_CURSOR)); + } + + private void visitText(ActiveTextCollector activeTextCollector) { + int lines = Math.min(this.height / this.font.lineHeight, this.lines.size()); + for (int i = 0; i < lines; ++i) { + activeTextCollector.accept(this.x, this.y + i * font.lineHeight, this.lines.get(i)); + } + } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean dbl) { + if (event.button() == 0) { + ActiveTextCollector.ClickableStyleFinder clickableStyleFinder + = new ActiveTextCollector.ClickableStyleFinder(this.font, (int) event.x(), (int) event.y()); + this.visitText(clickableStyleFinder); + + Style style = clickableStyleFinder.result(); + if (style != null && style.getClickEvent() != null) { + this.handleClickEvent.accept(style.getClickEvent()); + return true; + } + } + + return TextArea.super.mouseClicked(event, dbl); + } + + @Override + public void setText(Component text) { + this.text = text; + + FormattedText formattedText = ComponentUtils.mergeStyles(text, PAGE_TEXT_STYLE); + this.lines = this.font.split(formattedText, this.width); + } + + @Override + public void setVisible(boolean visible) { + this.visible = visible; + } + + @Override + public NarrationPriority narrationPriority() { + return this.hovered ? NarrationPriority.HOVERED : NarrationPriority.NONE; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) { + narrationElementOutput.add(NarratedElementType.TITLE, this.text); + } + + private boolean areCoordinatesInRectangle(double x, double y) { + return x >= this.x && y >= this.y && x < this.x + this.width && y < this.y + this.height; + } + + @Override + public boolean isMouseOver(double x, double y) { + return this.isActive() && this.areCoordinatesInRectangle(x, y); + } + + @Override + public ScreenRectangle getRectangle() { + return new ScreenRectangle(this.x, this.y, this.width, this.height); + } + + @Override + public boolean shouldTakeFocusAfterInteraction() { + return false; + } + + @Override + public boolean isActive() { + return this.visible; + } + + @Override + public void setFocused(boolean bl) { + // no-op: it's unfocusable. + } + + @Override + public boolean isFocused() { + return false; + } +} diff --git a/src/main/java/me/chrr/scribble/gui/PageNumberWidget.java b/src/main/java/me/chrr/scribble/gui/PageNumberWidget.java index 0aef564..5abfba1 100644 --- a/src/main/java/me/chrr/scribble/gui/PageNumberWidget.java +++ b/src/main/java/me/chrr/scribble/gui/PageNumberWidget.java @@ -18,10 +18,12 @@ import net.minecraft.sounds.SoundEvents; import net.minecraft.util.CommonColors; import net.minecraft.util.Util; +import org.jspecify.annotations.NullMarked; import org.lwjgl.glfw.GLFW; import java.util.function.Consumer; +@NullMarked public class PageNumberWidget extends AbstractWidget { private final Font font; private final Consumer onPageChange; diff --git a/src/main/java/me/chrr/scribble/gui/TextArea.java b/src/main/java/me/chrr/scribble/gui/TextArea.java new file mode 100644 index 0000000..86028fd --- /dev/null +++ b/src/main/java/me/chrr/scribble/gui/TextArea.java @@ -0,0 +1,13 @@ +package me.chrr.scribble.gui; + +import net.minecraft.client.gui.components.Renderable; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public interface TextArea extends Renderable, NarratableEntry, GuiEventListener { + void setText(T text); + + void setVisible(boolean visible); +} diff --git a/src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java b/src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java index b49b060..db36d0b 100644 --- a/src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java +++ b/src/main/java/me/chrr/scribble/gui/button/ColorSwatchWidget.java @@ -7,19 +7,20 @@ import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.input.InputWithModifiers; import net.minecraft.network.chat.Component; +import org.jspecify.annotations.NullMarked; +@NullMarked public class ColorSwatchWidget extends AbstractButton { private final ChatFormatting color; private final Runnable onClick; - private boolean toggled; + private boolean toggled = false; - public ColorSwatchWidget(Component tooltip, ChatFormatting color, Runnable onClick, int x, int y, int width, int height, boolean toggled) { + public ColorSwatchWidget(Component tooltip, ChatFormatting color, Runnable onClick, int x, int y, int width, int height) { super(x, y, width, height, tooltip); this.setTooltip(Tooltip.create(tooltip)); this.color = color; this.onClick = onClick; - this.toggled = toggled; } @Override diff --git a/src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java b/src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java index 537226e..8abb077 100644 --- a/src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java +++ b/src/main/java/me/chrr/scribble/gui/button/IconButtonWidget.java @@ -9,7 +9,9 @@ import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.network.chat.Component; import net.minecraft.resources.Identifier; +import org.jspecify.annotations.NullMarked; +@NullMarked public class IconButtonWidget extends AbstractButton { private static final Identifier WIDGETS_TEXTURE = Scribble.id("textures/gui/scribble_widgets.png"); diff --git a/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java b/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java index d7caa96..db70e7d 100644 --- a/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java +++ b/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java @@ -9,6 +9,7 @@ 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.function.Consumer; @@ -16,22 +17,22 @@ * A toggle-able button that's used in the book edit screen for toggling * text modifiers. It always uses `gui/scribble_widgets.png` as texture. */ +@NullMarked public class ModifierButtonWidget extends AbstractButton { private static final Identifier WIDGETS_TEXTURE = Scribble.id("textures/gui/scribble_widgets.png"); private final int u; private final int v; - public boolean toggled; + public boolean toggled = false; private final Consumer onToggle; - public ModifierButtonWidget(Component tooltip, Consumer onToggle, int x, int y, int u, int v, int width, int height, boolean toggled) { + public ModifierButtonWidget(Component tooltip, Consumer onToggle, int x, int y, int u, int v, int width, int height) { super(x, y, width, height, tooltip); this.setTooltip(Tooltip.create(tooltip)); this.u = u; this.v = v; - this.toggled = toggled; this.onToggle = onToggle; } diff --git a/src/main/java/me/chrr/scribble/gui/edit/RichEditBoxWidget.java b/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java similarity index 80% rename from src/main/java/me/chrr/scribble/gui/edit/RichEditBoxWidget.java rename to src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java index dcc58c5..81f76e4 100644 --- a/src/main/java/me/chrr/scribble/gui/edit/RichEditBoxWidget.java +++ b/src/main/java/me/chrr/scribble/gui/edit/RichEditBox.java @@ -1,7 +1,10 @@ package me.chrr.scribble.gui.edit; import com.mojang.blaze3d.platform.cursor.CursorTypes; +import com.mojang.datafixers.util.Pair; 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.gui.Font; @@ -14,10 +17,9 @@ import net.minecraft.client.input.KeyEvent; import net.minecraft.network.chat.Component; import net.minecraft.util.CommonColors; -import net.minecraft.util.Tuple; import net.minecraft.util.Util; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.lwjgl.glfw.GLFW; import java.util.HashSet; @@ -26,27 +28,26 @@ import java.util.Set; import java.util.function.Consumer; -public class RichEditBoxWidget extends MultiLineEditBox { - @Nullable - private final Runnable onInvalidateFormat; - @Nullable - private final Consumer onHistoryPush; +@NullMarked +public class RichEditBox extends MultiLineEditBox implements TextArea { + private final @Nullable Runnable onInvalidateFormat; + private final @Nullable Consumer onHistoryPush; - @Nullable - public ChatFormatting color = ChatFormatting.BLACK; + public @Nullable ChatFormatting color = ChatFormatting.BLACK; public Set modifiers = new HashSet<>(); - private RichEditBoxWidget(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) { + 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) { super(font, x, y, width, height, placeholder, message, textColor, textShadow, cursorColor, hasBackground, hasOverlay); + this.onInvalidateFormat = onInvalidateFormat; this.onHistoryPush = onHistoryPush; this.textField = new RichMultiLineTextField( font, width - this.totalInnerPadding(), - () -> new Tuple<>(Optional.ofNullable(color).orElse(ChatFormatting.BLACK), modifiers), + () -> new Pair<>(Optional.ofNullable(color).orElse(ChatFormatting.BLACK), modifiers), (color, modifiers) -> { this.color = color; this.modifiers = new HashSet<>(modifiers); @@ -66,12 +67,16 @@ private void pushHistory(EditCommand command) { } } - public void applyFormatting(ChatFormatting formatting, boolean active) { - RichMultiLineTextField editBox = this.getRichTextField(); + public void setRichValueListener(Consumer valueListener) { + this.getRichTextField().setRichValueListener(valueListener); + } + + public void applyFormat(ChatFormatting formatting, boolean active) { + RichMultiLineTextField textField = this.getRichTextField(); - if (editBox.hasSelection()) { - EditCommand command = new EditCommand(editBox, (box) -> box.applyFormatting(formatting, active)); - command.executeEdit(editBox); + if (textField.hasSelection()) { + EditCommand command = new EditCommand(this, (box) -> box.applyFormatting(formatting, active)); + command.executeEdit(textField); this.pushHistory(command); } else { if (formatting.isFormat()) { @@ -103,7 +108,7 @@ protected void renderContents(GuiGraphics graphics, int mouseX, int mouseY, floa // 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(), -857677600); + graphics.drawWordWrap(this.font, this.placeholder, this.getInnerLeft(), this.getInnerTop(), this.width - this.totalInnerPadding(), 0xcce0e0e0); return; } @@ -192,8 +197,8 @@ protected void renderContents(GuiGraphics graphics, int mouseX, int mouseY, floa @Override public boolean charTyped(CharacterEvent event) { if (this.visible && this.isFocused() && event.isAllowedChatCharacter()) { - EditCommand command = new EditCommand(this.getRichTextField(), - (editBox) -> editBox.insertText(event.codepointAsString())); + EditCommand command = new EditCommand(this, + (textField) -> textField.insertText(event.codepointAsString())); command.executeEdit(this.getRichTextField()); this.pushHistory(command); return true; @@ -216,7 +221,7 @@ public boolean keyPressed(KeyEvent event) { }; if (modifier != null) { - this.applyFormatting(modifier, !this.modifiers.contains(modifier)); + this.applyFormat(modifier, !this.modifiers.contains(modifier)); return true; } } @@ -225,8 +230,8 @@ public boolean keyPressed(KeyEvent event) { 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.getRichTextField(), - (editBox) -> editBox.keyPressed(event)); + EditCommand command = new EditCommand(this, + (textField) -> textField.keyPressed(event)); command.executeEdit(this.getRichTextField()); this.pushHistory(command); return true; @@ -244,28 +249,38 @@ public void updateWidgetNarration(NarrationElementOutput narrationElementOutput) } public RichMultiLineTextField getRichTextField() { - return (RichMultiLineTextField) textField; + return (RichMultiLineTextField) this.textField; + } + + @Override + public void setText(RichText text) { + this.getRichTextField().setValue(text, true); + } + + @Override + public void setVisible(boolean visible) { + this.visible = visible; } public static class Builder extends MultiLineEditBox.Builder { @Nullable private Runnable onInvalidateFormat = null; @Nullable - private Consumer onHistoryPush = null; + private Consumer onHistoryPush = null; public Builder onInvalidateFormat(Runnable onInvalidateFormat) { this.onInvalidateFormat = onInvalidateFormat; return this; } - public Builder onHistoryPush(Consumer onHistoryPush) { + public Builder onHistoryPush(Consumer onHistoryPush) { this.onHistoryPush = onHistoryPush; return this; } @Override - public @NotNull MultiLineEditBox build(Font font, int width, int height, Component message) { - return new RichEditBoxWidget(font, + 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, diff --git a/src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java b/src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java index f7b9cea..ea01293 100644 --- a/src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java +++ b/src/main/java/me/chrr/scribble/gui/edit/RichMultiLineTextField.java @@ -1,5 +1,6 @@ package me.chrr.scribble.gui.edit; +import com.mojang.datafixers.util.Pair; import me.chrr.scribble.KeyboardUtil; import me.chrr.scribble.Scribble; import me.chrr.scribble.book.RichText; @@ -11,25 +12,28 @@ import net.minecraft.client.input.KeyEvent; import net.minecraft.network.chat.Style; import net.minecraft.util.Mth; -import net.minecraft.util.Tuple; import org.apache.commons.lang3.mutable.MutableInt; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; +@NullMarked public class RichMultiLineTextField extends MultilineTextField { - private final Supplier>> formatSupplier; - private final BiConsumer<@Nullable ChatFormatting, Set> formatListener; + // These assignments are used, as super() in the constructor calls functions before they are assigned. + @SuppressWarnings("UnusedAssignment") + private @Nullable Supplier>> formatSupplier = null; + @SuppressWarnings("UnusedAssignment") + private @Nullable BiConsumer<@Nullable ChatFormatting, Set> formatListener = null; - private RichText richText; + private RichText richText = RichText.EMPTY; public RichMultiLineTextField( Font font, int width, - Supplier>> formatSupplier, + Supplier>> formatSupplier, BiConsumer<@Nullable ChatFormatting, Set> formatListener ) { super(font, width); @@ -43,11 +47,15 @@ public void setValueListener(Consumer valueListener) { super.setValueListener((text) -> valueListener.accept(value())); } + public void setRichValueListener(Consumer valueListener) { + super.setValueListener((text) -> valueListener.accept(getRichText())); + } + public void sendUpdateFormat() { if (this.formatListener != null) { StringView selection = this.getSelected(); - Tuple<@Nullable ChatFormatting, Set> format = this.richText.getCommonFormat(selection.beginIndex(), selection.endIndex()); - this.formatListener.accept(format.getA(), format.getB()); + Pair<@Nullable ChatFormatting, Set> format = this.richText.getCommonFormat(selection.beginIndex(), selection.endIndex()); + this.formatListener.accept(format.getFirst(), format.getSecond()); } } @@ -79,10 +87,12 @@ public void applyFormatting(ChatFormatting formatting, boolean active) { public void setValue(String text, boolean allowOverflow) { String truncated = this.truncateFullText(text); RichText richText = RichText.fromFormattedString(truncated); + this.setValue(richText, allowOverflow); + } + public void setValue(RichText richText, boolean allowOverflow) { if (allowOverflow || !this.overflowsLineLimit(richText)) { - this.richText = richText; - this.value = richText.getPlainText(); + this.setValueWithoutUpdating(richText); this.cursor = this.value.length(); this.selectCursor = this.cursor; @@ -92,16 +102,19 @@ public void setValue(String text, boolean allowOverflow) { } } - @Override - public @NotNull String value() { - return richText.getAsFormattedString(); - } - - public void setRichTextWithoutUpdating(RichText richText) { + public void setValueWithoutUpdating(RichText richText) { this.richText = richText; this.value = richText.getPlainText(); } + public void resetCursor(boolean update) { + this.cursor = this.value.length(); + this.selectCursor = this.cursor; + + if (update) + this.sendUpdateFormat(); + } + @Override public void insertText(String string) { // We consider the RESET formatting code to be void, as it messes with books. @@ -113,9 +126,10 @@ public void insertText(String string) { // If the string contains formatting codes, we keep them in. Otherwise, // we just type in the current color and modifiers. - Tuple> style = this.formatSupplier.get(); + assert this.formatSupplier != null; + Pair> style = this.formatSupplier.get(); RichText replacement = ChatFormatting.stripFormatting(string).equals(string) - ? new RichText(string, style.getA(), style.getB()) + ? new RichText(string, style.getFirst(), style.getSecond()) : RichText.fromFormattedString(string); StringView substring = this.getSelected(); @@ -168,7 +182,7 @@ public void seekCursor(Whence whence, int amount) { @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_MANAGER.getConfig().copyFormattingCodes ^ event.hasShiftDown(); + boolean keepFormatting = Scribble.config().copyFormattingCodes ^ event.hasShiftDown(); boolean ctrlNoAlt = event.hasControlDown() && !event.hasAltDown(); if (ctrlNoAlt && (KeyboardUtil.isKey(event.key(), "C") || KeyboardUtil.isKey(event.key(), "X"))) { String text = this.getSelectedText(); @@ -222,16 +236,21 @@ protected void reflowDisplayLines() { } @Override - public @NotNull String getSelectedText() { + public String getSelectedText() { StringView substring = this.getSelected(); return this.richText.subText(substring.beginIndex(), substring.endIndex()).getAsFormattedString(); } - private boolean overflowsLineLimit(RichText text) { - return this.hasLineLimit() && this.font.getSplitter().splitLines(text, this.width, Style.EMPTY).size() > this.lineLimit; - } - public RichText getRichText() { return richText; } + + @Override + public String value() { + return richText.getAsFormattedString(); + } + + private boolean overflowsLineLimit(RichText text) { + return this.hasLineLimit() && this.font.getSplitter().splitLines(text, this.width, Style.EMPTY).size() > this.lineLimit; + } } diff --git a/src/main/java/me/chrr/scribble/history/CommandManager.java b/src/main/java/me/chrr/scribble/history/CommandManager.java index d173a0d..2f6d3b6 100644 --- a/src/main/java/me/chrr/scribble/history/CommandManager.java +++ b/src/main/java/me/chrr/scribble/history/CommandManager.java @@ -2,10 +2,12 @@ import me.chrr.scribble.Scribble; import me.chrr.scribble.history.command.Command; +import org.jspecify.annotations.NullMarked; import java.util.ArrayList; import java.util.List; +@NullMarked public class CommandManager { private final HistoryListener listener; @@ -24,7 +26,7 @@ public void push(Command command) { this.commands.add(command); this.index += 1; - if (this.commands.size() > Scribble.CONFIG_MANAGER.getConfig().editHistorySize) { + if (this.commands.size() > Scribble.config().editHistorySize) { this.commands.removeFirst(); this.index -= 1; } @@ -55,4 +57,9 @@ public void tryRedo() { command.execute(this.listener); this.index += 1; } + + public void clear() { + this.commands.clear(); + this.index = 0; + } } diff --git a/src/main/java/me/chrr/scribble/history/HistoryListener.java b/src/main/java/me/chrr/scribble/history/HistoryListener.java index bf33856..c438fd3 100644 --- a/src/main/java/me/chrr/scribble/history/HistoryListener.java +++ b/src/main/java/me/chrr/scribble/history/HistoryListener.java @@ -3,18 +3,18 @@ import me.chrr.scribble.book.RichText; import me.chrr.scribble.gui.edit.RichMultiLineTextField; import net.minecraft.ChatFormatting; -import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import java.util.Set; +@NullMarked public interface HistoryListener { - void scribble$history$switchPage(int page); + RichMultiLineTextField switchAndFocusPage(int page); - void scribble$history$setFormat(@Nullable ChatFormatting color, Set modifiers); + void setFormat(@Nullable ChatFormatting color, Set modifiers); - void scribble$history$insertPage(int page, @Nullable RichText content); + void insertPageAt(int page, @Nullable RichText content); - void scribble$history$deletePage(int page); - - RichMultiLineTextField scribble$history$getRichEditBox(); + void deletePage(int page); } diff --git a/src/main/java/me/chrr/scribble/history/command/Command.java b/src/main/java/me/chrr/scribble/history/command/Command.java index 34cec78..ba95988 100644 --- a/src/main/java/me/chrr/scribble/history/command/Command.java +++ b/src/main/java/me/chrr/scribble/history/command/Command.java @@ -1,7 +1,9 @@ package me.chrr.scribble.history.command; import me.chrr.scribble.history.HistoryListener; +import org.jspecify.annotations.NullMarked; +@NullMarked public interface Command { void execute(HistoryListener listener); diff --git a/src/main/java/me/chrr/scribble/history/command/EditCommand.java b/src/main/java/me/chrr/scribble/history/command/EditCommand.java index c4c0d00..1957aad 100644 --- a/src/main/java/me/chrr/scribble/history/command/EditCommand.java +++ b/src/main/java/me/chrr/scribble/history/command/EditCommand.java @@ -1,65 +1,69 @@ package me.chrr.scribble.history.command; import me.chrr.scribble.book.RichText; +import me.chrr.scribble.gui.edit.RichEditBox; import me.chrr.scribble.gui.edit.RichMultiLineTextField; import me.chrr.scribble.history.HistoryListener; import net.minecraft.ChatFormatting; -import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import java.util.Set; import java.util.function.Consumer; +@NullMarked public class EditCommand implements Command { private final RichText text; private final Consumer action; public int page = -1; - @Nullable - public ChatFormatting color = null; - public Set modifiers = Set.of(); + public final @Nullable ChatFormatting color; + public final Set modifiers; private final int cursor; private final int selectCursor; private final boolean selecting; - public EditCommand(RichMultiLineTextField editBox, Consumer action) { - this.text = editBox.getRichText(); + public EditCommand(RichEditBox editBox, Consumer action) { + RichMultiLineTextField textField = editBox.getRichTextField(); + + this.text = textField.getRichText(); this.action = action; - this.cursor = editBox.cursor; - this.selectCursor = editBox.selectCursor; - this.selecting = editBox.selecting; + this.color = editBox.color; + this.modifiers = editBox.modifiers; + + this.cursor = textField.cursor; + this.selectCursor = textField.selectCursor; + this.selecting = textField.selecting; } - public void executeEdit(RichMultiLineTextField editBox) { - editBox.cursor = cursor; - editBox.selectCursor = selectCursor; - editBox.selecting = selecting; - action.accept(editBox); + public void executeEdit(RichMultiLineTextField textField) { + textField.cursor = this.cursor; + textField.selectCursor = this.selectCursor; + textField.selecting = this.selecting; + action.accept(textField); } @Override public void execute(HistoryListener listener) { - listener.scribble$history$switchPage(page); - listener.scribble$history$setFormat(this.color, this.modifiers); - - RichMultiLineTextField editBox = listener.scribble$history$getRichEditBox(); - this.executeEdit(editBox); + RichMultiLineTextField textField = listener.switchAndFocusPage(this.page); + listener.setFormat(this.color, this.modifiers); + this.executeEdit(textField); } @Override public void rollback(HistoryListener listener) { - listener.scribble$history$switchPage(page); - listener.scribble$history$setFormat(this.color, this.modifiers); + RichMultiLineTextField textField = listener.switchAndFocusPage(this.page); + listener.setFormat(this.color, this.modifiers); - RichMultiLineTextField editBox = listener.scribble$history$getRichEditBox(); - editBox.cursor = cursor; - editBox.selectCursor = selectCursor; - editBox.selecting = selecting; - editBox.setRichTextWithoutUpdating(text); + textField.cursor = this.cursor; + textField.selectCursor = this.selectCursor; + textField.selecting = this.selecting; + textField.setValueWithoutUpdating(this.text); - editBox.onValueChange(); - editBox.sendUpdateFormat(); + textField.onValueChange(); + textField.sendUpdateFormat(); } } 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 58d9582..ff17bab 100644 --- a/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java +++ b/src/main/java/me/chrr/scribble/history/command/PageDeleteCommand.java @@ -2,7 +2,9 @@ import me.chrr.scribble.book.RichText; import me.chrr.scribble.history.HistoryListener; +import org.jspecify.annotations.NullMarked; +@NullMarked public class PageDeleteCommand implements Command { private final int page; private final RichText content; @@ -14,11 +16,11 @@ public PageDeleteCommand(int page, RichText content) { @Override public void execute(HistoryListener listener) { - listener.scribble$history$deletePage(page); + listener.deletePage(page); } @Override public void rollback(HistoryListener listener) { - listener.scribble$history$insertPage(page, content); + listener.insertPageAt(page, content); } } 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 6939ee4..66b116d 100644 --- a/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java +++ b/src/main/java/me/chrr/scribble/history/command/PageInsertCommand.java @@ -1,7 +1,9 @@ package me.chrr.scribble.history.command; import me.chrr.scribble.history.HistoryListener; +import org.jspecify.annotations.NullMarked; +@NullMarked public class PageInsertCommand implements Command { private final int page; @@ -11,11 +13,11 @@ public PageInsertCommand(int page) { @Override public void execute(HistoryListener listener) { - listener.scribble$history$insertPage(page, null); + listener.insertPageAt(page, null); } @Override public void rollback(HistoryListener listener) { - listener.scribble$history$deletePage(page); + listener.deletePage(page); } } diff --git a/src/main/java/me/chrr/scribble/mixin/BookEditScreenMixin.java b/src/main/java/me/chrr/scribble/mixin/BookEditScreenMixin.java deleted file mode 100644 index d48b608..0000000 --- a/src/main/java/me/chrr/scribble/mixin/BookEditScreenMixin.java +++ /dev/null @@ -1,549 +0,0 @@ -package me.chrr.scribble.mixin; - -import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import me.chrr.scribble.KeyboardUtil; -import me.chrr.scribble.Scribble; -import me.chrr.scribble.book.BookFile; -import me.chrr.scribble.book.FileChooser; -import me.chrr.scribble.book.RichText; -import me.chrr.scribble.config.Config; -import me.chrr.scribble.gui.PageNumberWidget; -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.RichMultiLineTextField; -import me.chrr.scribble.gui.edit.RichEditBoxWidget; -import me.chrr.scribble.history.CommandManager; -import me.chrr.scribble.history.HistoryListener; -import me.chrr.scribble.history.command.EditCommand; -import me.chrr.scribble.history.command.PageDeleteCommand; -import me.chrr.scribble.history.command.PageInsertCommand; -import net.minecraft.ChatFormatting; -import net.minecraft.client.gui.ActiveTextCollector; -import net.minecraft.client.gui.TextAlignment; -import net.minecraft.client.gui.components.AbstractWidget; -import net.minecraft.client.gui.components.MultiLineEditBox; -import net.minecraft.client.gui.components.Renderable; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.narration.NarratableEntry; -import net.minecraft.client.gui.screens.ConfirmScreen; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.inventory.BookEditScreen; -import net.minecraft.client.input.KeyEvent; -import net.minecraft.client.input.MouseButtonEvent; -import net.minecraft.network.chat.Component; -import net.minecraft.world.entity.player.Player; -import org.jetbrains.annotations.Nullable; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.ModifyArg; -import org.spongepowered.asm.mixin.injection.Redirect; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Consumer; - -@Mixin(BookEditScreen.class) -public abstract class BookEditScreenMixin extends Screen implements HistoryListener { - //region Constants - @Unique - private static final ChatFormatting[] scribble$COLORS = new ChatFormatting[]{ - ChatFormatting.BLACK, ChatFormatting.DARK_GRAY, - ChatFormatting.GRAY, ChatFormatting.WHITE, - ChatFormatting.DARK_RED, ChatFormatting.RED, - ChatFormatting.GOLD, ChatFormatting.YELLOW, - ChatFormatting.DARK_GREEN, ChatFormatting.GREEN, - ChatFormatting.DARK_AQUA, ChatFormatting.AQUA, - ChatFormatting.DARK_BLUE, ChatFormatting.BLUE, - ChatFormatting.DARK_PURPLE, ChatFormatting.LIGHT_PURPLE, - }; - //endregion - - //region @Shadow declarations - @Shadow - private MultiLineEditBox page; - - @Shadow - @Final - private List pages; - - @Shadow - private int currentPage; - - @Shadow - @Final - private Player owner; - - @Shadow - protected abstract void updatePageContent(); - - @Shadow - protected abstract void updateButtonVisibility(); - //endregion - - //region Variables - @Unique - private ModifierButtonWidget scribble$boldButton; - @Unique - private ModifierButtonWidget scribble$italicButton; - @Unique - private ModifierButtonWidget scribble$underlineButton; - @Unique - private ModifierButtonWidget scribble$strikethroughButton; - @Unique - private ModifierButtonWidget scribble$obfuscatedButton; - - @Unique - private List scribble$colorSwatches = List.of(); - - @Unique - private IconButtonWidget scribble$deletePageButton; - @Unique - private IconButtonWidget scribble$insertPageButton; - - @Unique - private IconButtonWidget scribble$undoButton; - @Unique - private IconButtonWidget scribble$redoButton; - - @Unique - private PageNumberWidget scribble$pageNumberWidget; - - @Unique - private boolean scribble$dirty = false; - @Unique - private final CommandManager scribble$commandManager = new CommandManager(this); - //endregion - - // Dummy constructor to match super class. - private BookEditScreenMixin() { - super(null); - } - - @Redirect(method = "init", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/components/MultiLineEditBox;builder()Lnet/minecraft/client/gui/components/MultiLineEditBox$Builder;")) - public MultiLineEditBox.Builder buildEditBoxWidget() { - return new RichEditBoxWidget.Builder() - .onInvalidateFormat(this::scribble$invalidateFormattingButtons) - .onHistoryPush(this::scribble$pushEditCommand); - } - - @Unique - private RichEditBoxWidget scribble$getRichEditBoxWidget() { - return (RichEditBoxWidget) this.page; - } - - //region Formatting Buttons - @Inject(method = "init", at = @At(value = "TAIL")) - public void initFormattingButtons(CallbackInfo ci) { - int x = this.width / 2 + 78; - int y = Scribble.getBookScreenYOffset(height) + 12; - - // Modifier buttons - scribble$boldButton = scribble$addModifierButton( - ChatFormatting.BOLD, Component.translatable("text.scribble.modifier.bold"), - x, y, 0, 0, 22, 19); - scribble$italicButton = scribble$addModifierButton( - ChatFormatting.ITALIC, Component.translatable("text.scribble.modifier.italic"), - x, y + 19, 0, 19, 22, 17); - scribble$underlineButton = scribble$addModifierButton( - ChatFormatting.UNDERLINE, Component.translatable("text.scribble.modifier.underline"), - x, y + 36, 0, 36, 22, 17); - scribble$strikethroughButton = scribble$addModifierButton( - ChatFormatting.STRIKETHROUGH, Component.translatable("text.scribble.modifier.strikethrough"), - x, y + 53, 0, 53, 22, 17); - scribble$obfuscatedButton = scribble$addModifierButton( - ChatFormatting.OBFUSCATED, Component.translatable("text.scribble.modifier.obfuscated"), - x, y + 70, 0, 70, 22, 18); - - // Color swatches - RichEditBoxWidget editBox = scribble$getRichEditBoxWidget(); - scribble$colorSwatches = new ArrayList<>(scribble$COLORS.length); - for (int i = 0; i < scribble$COLORS.length; i++) { - ChatFormatting color = scribble$COLORS[i]; - - int dx = (i % 2) * 8; - int dy = (i / 2) * 8; - - ColorSwatchWidget swatch = new ColorSwatchWidget( - Component.translatable("text.scribble.color." + color.getName()), color, - () -> editBox.applyFormatting(color, true), - x + 3 + dx, y + 95 + dy, 8, 8, - editBox.color == color - ); - - ColorSwatchWidget widget = addRenderableWidget(swatch); - scribble$colorSwatches.add(widget); - } - - scribble$invalidateFormattingButtons(); - } - - @Unique - private ModifierButtonWidget scribble$addModifierButton(ChatFormatting modifier, Component tooltip, int x, int y, int u, int v, int width, int height) { - RichEditBoxWidget editBox = scribble$getRichEditBoxWidget(); - ModifierButtonWidget button = new ModifierButtonWidget( - tooltip, (toggled) -> editBox.applyFormatting(modifier, toggled), - x, y, u, v, width, height, - editBox.modifiers.contains(modifier)); - return addRenderableWidget(button); - } - - @Unique - private void scribble$invalidateFormattingButtons() { - RichEditBoxWidget editBox = scribble$getRichEditBoxWidget(); - - // Sometimes, these buttons get invalidated on screen initialize, when the buttons don't exist yet. - // We can just return in that case. - if (scribble$boldButton == null) { - return; - } - - scribble$boldButton.toggled = editBox.modifiers.contains(ChatFormatting.BOLD); - scribble$italicButton.toggled = editBox.modifiers.contains(ChatFormatting.ITALIC); - scribble$underlineButton.toggled = editBox.modifiers.contains(ChatFormatting.UNDERLINE); - scribble$strikethroughButton.toggled = editBox.modifiers.contains(ChatFormatting.STRIKETHROUGH); - scribble$obfuscatedButton.toggled = editBox.modifiers.contains(ChatFormatting.OBFUSCATED); - - scribble$setSwatchColor(editBox.color); - } - - @Unique - private void scribble$setSwatchColor(ChatFormatting color) { - for (ColorSwatchWidget swatch : scribble$colorSwatches) { - swatch.setToggled(swatch.getColor() == color); - } - } - //endregion - - //region Page Buttons - @Inject(method = "init", at = @At(value = "HEAD")) - public void initPageButtons(CallbackInfo ci) { - int x = this.width / 2 - 96; - int y = Scribble.getBookScreenYOffset(height) + 12; - - scribble$deletePageButton = addRenderableWidget(new IconButtonWidget( - Component.translatable("text.scribble.action.delete_page"), - this::scribble$deletePage, - x + 94, y + 148, 0, 90, 12, 12)); - scribble$insertPageButton = addRenderableWidget(new IconButtonWidget( - Component.translatable("text.scribble.action.insert_new_page"), - this::scribble$insertPage, - x + 78, y + 148, 12, 90, 12, 12)); - } - - @Inject(method = "updateButtonVisibility", at = @At(value = "HEAD")) - public void invalidatePageButtons(CallbackInfo ci) { - scribble$deletePageButton.visible = this.pages.size() > 1; - scribble$insertPageButton.visible = this.pages.size() < 100; - } - - @Unique - private void scribble$deletePage() { - if (this.pages.size() > 1) { - // See scribble$history$deletePage for implementation. - PageDeleteCommand command = new PageDeleteCommand(this.currentPage, - this.scribble$getRichEditBoxWidget().getRichTextField().getRichText()); - command.execute(this); - - scribble$commandManager.push(command); - this.scribble$dirty = true; - } - } - - @Unique - private void scribble$insertPage() { - if (this.pages.size() < 100) { - // See scribble$history$insertPage for implementation. - PageInsertCommand command = new PageInsertCommand(this.currentPage); - command.execute(this); - - scribble$commandManager.push(command); - this.scribble$dirty = true; - } - } - //endregion - - //region Action Buttons - @Inject(method = "init", at = @At(value = "TAIL")) - public void initActionButtons(CallbackInfo ci) { - int ax = this.width / 2 - 78 - 7 - 12; - int ay = Scribble.getBookScreenYOffset(height) + 12 + 4; - - scribble$undoButton = addRenderableWidget(new IconButtonWidget( - Component.translatable("text.scribble.action.undo"), - () -> { - scribble$commandManager.tryUndo(); - this.scribble$invalidateActionButtons(); - }, - ax, ay, 24, 90, 12, 12)); - scribble$redoButton = addRenderableWidget(new IconButtonWidget( - Component.translatable("text.scribble.action.redo"), - () -> { - scribble$commandManager.tryRedo(); - this.scribble$invalidateActionButtons(); - }, - ax, ay + 12, 36, 90, 12, 12)); - - if (Scribble.CONFIG_MANAGER.getConfig().showActionButtons != Config.ShowActionButtons.NEVER) { - addRenderableWidget(new IconButtonWidget( - Component.translatable("text.scribble.action.save_book_to_file"), - () -> FileChooser.chooseBook(true, this::scribble$saveTo), - ax, ay + 12 * 2 + 4, 48, 90, 12, 12)); - addRenderableWidget(new IconButtonWidget( - Component.translatable("text.scribble.action.load_book_from_file"), - () -> this.scribble$confirmIf(true, "overwrite_warning", - () -> FileChooser.chooseBook(false, this::scribble$loadFrom)), - ax, ay + 12 * 3 + 4, 60, 90, 12, 12)); - } - - scribble$invalidateActionButtons(); - } - - @Unique - private void scribble$invalidateActionButtons() { - boolean show = Scribble.CONFIG_MANAGER.getConfig().showActionButtons != Config.ShowActionButtons.NEVER; - scribble$undoButton.visible = show; - scribble$redoButton.visible = show; - - scribble$undoButton.active = scribble$commandManager.canUndo(); - scribble$redoButton.active = scribble$commandManager.canRedo(); - } - - @Inject(method = "keyPressed", at = @At(value = "HEAD"), cancellable = true) - public void onActionKeyPressed(KeyEvent event, CallbackInfoReturnable cir) { - if (event.hasControlDown() && !event.hasAltDown()) { - if ((KeyboardUtil.isKey(event.key(), "Z") && !event.hasShiftDown() && scribble$undoButton.active)) { - scribble$undoButton.onPress(event); - cir.setReturnValue(true); - } else if (((KeyboardUtil.isKey(event.key(), "Z") && event.hasShiftDown()) || (KeyboardUtil.isKey(event.key(), "Y") && !event.hasShiftDown())) && scribble$redoButton.active) { - scribble$redoButton.onPress(event); - cir.setReturnValue(true); - } - } - } - //endregion - - //region Page Number Widget - @Inject(method = "init", at = @At(value = "HEAD")) - public void initPageNumberWidget(CallbackInfo ci) { - int x = (this.width - 192) / 2; - int y = Scribble.getBookScreenYOffset(height); - - this.scribble$pageNumberWidget = addRenderableWidget( - new PageNumberWidget( - (page) -> { - this.currentPage = Math.clamp(page - 1, 0, this.pages.size() - 1); - this.updatePageContent(); - this.updateButtonVisibility(); - this.setFocused(this.page); - }, - x + 192 - 44, y + 18, this.font)); - } - - @Inject(method = "updatePageContent", at = @At(value = "HEAD")) - public void updatePageNumber(CallbackInfo ci) { - this.scribble$pageNumberWidget.setPageNumber(this.currentPage + 1, this.pages.size()); - } - - @WrapWithCondition(method = "visitText", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/ActiveTextCollector;accept(Lnet/minecraft/client/gui/TextAlignment;IILnet/minecraft/network/chat/Component;)V")) - public boolean drawIndicatorText(ActiveTextCollector instance, TextAlignment textAlignment, int i, int j, Component component) { - // Do nothing: this is replaced by scribble$pageNumberWidget. - return false; - } - //endregion - - //region History - @Override - public void scribble$history$switchPage(int page) { - if (page < 0 || page >= this.pages.size()) - return; - - this.currentPage = page; - this.updatePageContent(); - this.updateButtonVisibility(); - } - - @Override - public void scribble$history$setFormat(@Nullable ChatFormatting color, Set modifiers) { - RichEditBoxWidget editBox = this.scribble$getRichEditBoxWidget(); - editBox.color = color; - editBox.modifiers = new HashSet<>(modifiers); - this.scribble$invalidateFormattingButtons(); - } - - @Override - public void scribble$history$insertPage(int page, @Nullable RichText content) { - this.scribble$history$switchPage(page); - - String text = ""; - if (content != null) - text = content.getAsFormattedString(); - - this.pages.add(page, text); - this.updatePageContent(); - this.updateButtonVisibility(); - } - - @Override - public void scribble$history$deletePage(int page) { - this.scribble$history$switchPage(page); - - this.pages.remove(page); - if (this.currentPage == this.pages.size()) { - this.currentPage -= 1; - } - - this.updatePageContent(); - this.updateButtonVisibility(); - } - - @Override - public RichMultiLineTextField scribble$history$getRichEditBox() { - return this.scribble$getRichEditBoxWidget().getRichTextField(); - } - - @Unique - private void scribble$pushEditCommand(EditCommand command) { - RichEditBoxWidget editBox = this.scribble$getRichEditBoxWidget(); - - command.page = this.currentPage; - command.color = editBox.color; - command.modifiers = editBox.modifiers; - - this.scribble$commandManager.push(command); - this.scribble$invalidateActionButtons(); - } - //endregion - - //region Skip to first/last page - // When shift is held down, skip to the last page. - @Inject(method = "pageForward", at = @At(value = "HEAD"), cancellable = true) - public void pageForward(CallbackInfo ci) { - int lastPage = this.pages.size() - 1; - if (this.currentPage < lastPage && KeyboardUtil.hasShiftDown()) { - this.currentPage = lastPage; - this.updatePageContent(); - this.updateButtonVisibility(); - ci.cancel(); - } - } - - // When shift is held down, skip to the first page. - @Inject(method = "pageBack", at = @At(value = "HEAD"), cancellable = true) - public void pageBack(CallbackInfo ci) { - if (KeyboardUtil.hasShiftDown()) { - this.currentPage = 0; - this.updatePageContent(); - this.updateButtonVisibility(); - ci.cancel(); - } - } - //endregion - - //region Saving and Loading - @Unique - private void scribble$saveTo(Path path) { - try { - BookFile bookFile = new BookFile(this.owner.getName().getString(), this.pages); - bookFile.writeJson(path); - } catch (Exception e) { - Scribble.LOGGER.error("could not save book to file", e); - } - } - - @Unique - private void scribble$loadFrom(Path path) { - try { - BookFile bookFile = BookFile.readFile(path); - - this.pages.clear(); - this.pages.addAll(bookFile.pages()); - this.currentPage = 0; - - this.updatePageContent(); - this.updateButtonVisibility(); - this.scribble$dirty = true; - } catch (Exception e) { - Scribble.LOGGER.error("could not load book from file", e); - } - } - //endregion - - //region Overwrite / Close confirmations - // We want to keep track of if the book has been edited (i.e. if it's "dirty"). - @ModifyArg(method = "init", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/components/MultiLineEditBox;setValueListener(Ljava/util/function/Consumer;)V"), index = 0) - public Consumer modifyValueListener(Consumer valueListener) { - return (text) -> { - if (!text.equals(this.pages.get(this.currentPage))) { - this.scribble$dirty = true; - } - - valueListener.accept(text); - }; - } - - // Show a confirmation dialog if passed condition is true, otherwise run the runnable immediately. - @Unique - public void scribble$confirmIf(boolean condition, String name, Runnable runnable) { - if (condition && this.minecraft != null) { - this.minecraft.setScreen(new ConfirmScreen( - confirmed -> { - this.minecraft.setScreen(this); - if (confirmed) runnable.run(); - }, - Component.translatable("text.scribble." + name + ".title"), - Component.translatable("text.scribble." + name + ".description") - )); - } else { - runnable.run(); - } - } - - // Show a confirmation dialog when trying to exit the screen if the book has been edited - @Override - public void onClose() { - this.scribble$confirmIf(this.scribble$dirty, "quit_without_saving", super::onClose); - } - //endregion - - //region GUI Centering - // If we need to center the GUI, we shift the Y of the texture draw call down. - @ModifyArg(method = "renderBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;blit(Lcom/mojang/blaze3d/pipeline/RenderPipeline;Lnet/minecraft/resources/Identifier;IIFFIIII)V"), index = 3) - public int shiftBackgroundY(int y) { - return Scribble.getBookScreenYOffset(height) + y; - } - - // If we need to center the GUI, we shift the Y of the buttons down. - @WrapOperation(method = "init", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/BookEditScreen;addRenderableWidget(Lnet/minecraft/client/gui/components/events/GuiEventListener;)Lnet/minecraft/client/gui/components/events/GuiEventListener;")) - public T shiftButtonY(BookEditScreen instance, T guiEventListener, Operation original) { - if (guiEventListener instanceof AbstractWidget widget) { - widget.setY(widget.getY() + Scribble.getBookScreenYOffset(height)); - } - - return original.call(instance, guiEventListener); - } - //endregion - - //region Bug fixes - // We cancel any drags outside the width of the book interface. - @Override - public boolean mouseDragged(MouseButtonEvent event, double offsetX, double offsetY) { - if (event.x() < (this.width - 152) / 2.0 || event.x() > (this.width + 152) / 2.0) { - return true; - } else { - return super.mouseDragged(event, offsetX, offsetY); - } - } - //endregion -} diff --git a/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java b/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java index bad9d19..20113c0 100644 --- a/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java +++ b/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java @@ -2,52 +2,29 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import me.chrr.scribble.Scribble; -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; -import net.minecraft.client.gui.narration.NarratableEntry; +import me.chrr.scribble.SetReturnScreen; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.BookSignScreen; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.*; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +@NullMarked @Mixin(BookSignScreen.class) -public abstract class BookSignScreenMixin extends Screen { - // Dummy constructor to match super class. - private BookSignScreenMixin() { - super(null); - } - - // If we need to center the GUI, we shift the Y of the texture draw call down. - @ModifyArg(method = "renderBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;blit(Lcom/mojang/blaze3d/pipeline/RenderPipeline;Lnet/minecraft/resources/Identifier;IIFFIIII)V"), index = 3) - public int shiftBackgroundY(int y) { - return Scribble.getBookScreenYOffset(height) + y; - } - - // If we need to center the GUI, we shift the Y of the close buttons down. - @WrapOperation(method = "init", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/BookSignScreen;addRenderableWidget(Lnet/minecraft/client/gui/components/events/GuiEventListener;)Lnet/minecraft/client/gui/components/events/GuiEventListener;")) - public T shiftButtonY(BookSignScreen instance, T guiEventListener, Operation original) { - if (guiEventListener instanceof AbstractWidget widget) { - widget.setY(widget.getY() + Scribble.getBookScreenYOffset(height)); - } - - return original.call(instance, guiEventListener); - } +public abstract class BookSignScreenMixin implements SetReturnScreen { + @Unique + public @Nullable Screen scribble$returnScreen = null; - // 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) { - graphics.pose().pushMatrix(); - graphics.pose().translate(0f, Scribble.getBookScreenYOffset(height)); + @Override + public void scribble$setReturnScreen(Screen screen) { + this.scribble$returnScreen = screen; } - // 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) { - graphics.pose().popMatrix(); + @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); } } diff --git a/src/main/java/me/chrr/scribble/mixin/BookViewScreenMixin.java b/src/main/java/me/chrr/scribble/mixin/BookViewScreenMixin.java deleted file mode 100644 index 052db72..0000000 --- a/src/main/java/me/chrr/scribble/mixin/BookViewScreenMixin.java +++ /dev/null @@ -1,152 +0,0 @@ -package me.chrr.scribble.mixin; - -import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import me.chrr.scribble.Scribble; -import me.chrr.scribble.book.BookFile; -import me.chrr.scribble.book.FileChooser; -import me.chrr.scribble.book.RichText; -import me.chrr.scribble.config.Config; -import me.chrr.scribble.gui.PageNumberWidget; -import me.chrr.scribble.gui.button.IconButtonWidget; -import net.minecraft.client.gui.ActiveTextCollector; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.TextAlignment; -import net.minecraft.client.gui.components.AbstractWidget; -import net.minecraft.client.gui.components.Renderable; -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.gui.screens.inventory.BookViewScreen; -import net.minecraft.client.input.MouseButtonEvent; -import net.minecraft.network.chat.Component; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.*; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.util.List; - -@Mixin(BookViewScreen.class) -public abstract class BookViewScreenMixin extends Screen { - //region @Shadow declarations - @Shadow - private BookViewScreen.BookAccess bookAccess; - - @Shadow - private int currentPage; - - @Shadow - public abstract boolean setPage(int index); - //endregion - - @Unique - private PageNumberWidget scribble$pageNumberWidget; - - // Dummy constructor to match super class. - private BookViewScreenMixin() { - super(null); - } - - //region Action Buttons - @Inject(method = "init", at = @At(value = "TAIL")) - public void initButtons(CallbackInfo info) { - if (Scribble.CONFIG_MANAGER.getConfig().showActionButtons == Config.ShowActionButtons.ALWAYS) { - int x = this.width / 2 - 78 - 7 - 12; - int y = Scribble.getBookScreenYOffset(height) + 12 + 4; - - Runnable saveBook = () -> FileChooser.chooseBook(true, (path) -> { - try { - List pages = this.bookAccess.pages().stream() - .map(RichText::fromFormattedTextLossy) - .map(RichText::getAsFormattedString) - .toList(); - - BookFile bookFile = new BookFile("", pages); - bookFile.writeJson(path); - } catch (Exception e) { - Scribble.LOGGER.error("could not save book to file", e); - } - }); - - addRenderableWidget(new IconButtonWidget( - Component.translatable("text.scribble.action.save_book_to_file"), saveBook, - x, y, 48, 90, 12, 12)); - } - } - //endregion - - //region Page Number Widget - @Inject(method = "init", at = @At(value = "HEAD")) - public void initPageNumberWidget(CallbackInfo ci) { - int x = (this.width - 192) / 2; - int y = Scribble.getBookScreenYOffset(height); - - this.scribble$pageNumberWidget = addRenderableWidget( - new PageNumberWidget( - (page) -> setPage(page - 1), - x + 192 - 44, y + 18, this.font)); - } - - @Inject(method = "updateButtonVisibility", at = @At(value = "HEAD")) - public void updatePageNumber(CallbackInfo ci) { - this.scribble$pageNumberWidget.setPageNumber(this.currentPage + 1, this.bookAccess.getPageCount()); - } - - @WrapWithCondition(method = "visitText", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/ActiveTextCollector;accept(Lnet/minecraft/client/gui/TextAlignment;IILnet/minecraft/network/chat/Component;)V")) - public boolean drawIndicatorText(ActiveTextCollector instance, TextAlignment textAlignment, int i, int j, Component component) { - // Do nothing: this is replaced by scribble$pageNumberWidget. - return false; - } - //endregion - - //region GUI Centering - // If we need to center the GUI, we shift the Y of the texture draw call down. - @ModifyArg(method = "renderBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;blit(Lcom/mojang/blaze3d/pipeline/RenderPipeline;Lnet/minecraft/resources/Identifier;IIFFIIII)V"), index = 3) - public int shiftBackgroundY(int y) { - return Scribble.getBookScreenYOffset(height) + y; - } - - // If we need to center the GUI, we shift the Y of the close buttons down. - @WrapOperation(method = "createMenuControls", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/BookViewScreen;addRenderableWidget(Lnet/minecraft/client/gui/components/events/GuiEventListener;)Lnet/minecraft/client/gui/components/events/GuiEventListener;")) - public T shiftCloseButtonY(BookViewScreen instance, T guiEventListener, Operation original) { - if (guiEventListener instanceof AbstractWidget widget) { - widget.setY(widget.getY() + Scribble.getBookScreenYOffset(height)); - } - - return original.call(instance, guiEventListener); - } - - // If we need to center the GUI, we shift the Y of the page buttons down. - @WrapOperation(method = "createPageControlButtons", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/BookViewScreen;addRenderableWidget(Lnet/minecraft/client/gui/components/events/GuiEventListener;)Lnet/minecraft/client/gui/components/events/GuiEventListener;")) - public T shiftPageButtonY(BookViewScreen instance, T guiEventListener, Operation original) { - if (guiEventListener instanceof AbstractWidget widget) { - widget.setY(widget.getY() + Scribble.getBookScreenYOffset(height)); - } - - return original.call(instance, guiEventListener); - } - - // If we need to center the GUI, modify the coordinates to check. - @WrapOperation(method = "mouseClicked", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/input/MouseButtonEvent;y()D")) - public double shiftTextStyleY(MouseButtonEvent instance, Operation original) { - return original.call(instance) - Scribble.getBookScreenYOffset(height); - } - - // 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) { - graphics.pose().pushMatrix(); - graphics.pose().translate(0f, Scribble.getBookScreenYOffset(height)); - } - - // 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) { - graphics.pose().popMatrix(); - } - //endregion -} diff --git a/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java b/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java new file mode 100644 index 0000000..02e7cc9 --- /dev/null +++ b/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java @@ -0,0 +1,28 @@ +package me.chrr.scribble.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import me.chrr.scribble.screen.ScribbleBookViewScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.BookViewScreen; +import net.minecraft.client.multiplayer.ClientPacketListener; +import org.jspecify.annotations.NullMarked; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@NullMarked +@Mixin(ClientPacketListener.class) +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()) { + // FIXME: this is temporary, maybe a config option? + original.call(instance, screen); + } else { + // FIXME: ideally, I'd like to avoid even constructing the original BookViewScreen. + original.call(instance, new ScribbleBookViewScreen(book)); + } + } +} diff --git a/src/main/java/me/chrr/scribble/mixin/KeyboardHandlerMixin.java b/src/main/java/me/chrr/scribble/mixin/KeyboardHandlerMixin.java index e3cd063..b7a3af0 100644 --- a/src/main/java/me/chrr/scribble/mixin/KeyboardHandlerMixin.java +++ b/src/main/java/me/chrr/scribble/mixin/KeyboardHandlerMixin.java @@ -3,26 +3,29 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.llamalad7.mixinextras.sugar.Local; +import me.chrr.scribble.screen.ScribbleBookEditScreen; import net.minecraft.client.GameNarrator; import net.minecraft.client.KeyboardHandler; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screens.inventory.BookEditScreen; import net.minecraft.client.input.KeyEvent; +import org.jspecify.annotations.NullMarked; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; +@NullMarked @Mixin(KeyboardHandler.class) public class KeyboardHandlerMixin { @Shadow @Final private Minecraft minecraft; - // Disable the narrator hotkey when editing a book, and while not holding SHIFT. + // Disable the narrator hotkey when editing a book while not holding SHIFT, + // as it conflicts with the bold hotkey. @WrapOperation(method = "keyPress", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/GameNarrator;isActive()Z")) public boolean isNarratorActive(GameNarrator instance, Operation original, @Local(argsOnly = true) KeyEvent event) { - if (minecraft.screen instanceof BookEditScreen && !event.hasShiftDown()) { + if (minecraft.screen instanceof ScribbleBookEditScreen && !event.hasShiftDown()) { return false; } else { return original.call(instance); diff --git a/src/main/java/me/chrr/scribble/mixin/LecternScreenMixin.java b/src/main/java/me/chrr/scribble/mixin/LecternScreenMixin.java deleted file mode 100644 index dc86a15..0000000 --- a/src/main/java/me/chrr/scribble/mixin/LecternScreenMixin.java +++ /dev/null @@ -1,31 +0,0 @@ -package me.chrr.scribble.mixin; - -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import me.chrr.scribble.Scribble; -import net.minecraft.client.gui.components.AbstractWidget; -import net.minecraft.client.gui.components.Renderable; -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.gui.screens.inventory.LecternScreen; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; - -@Mixin(LecternScreen.class) -public abstract class LecternScreenMixin extends Screen { - // Dummy constructor to match super class. - private LecternScreenMixin() { - super(null); - } - - // If we need to center the GUI, we shift the Y of the close buttons down. - @WrapOperation(method = "createMenuControls", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/LecternScreen;addRenderableWidget(Lnet/minecraft/client/gui/components/events/GuiEventListener;)Lnet/minecraft/client/gui/components/events/GuiEventListener;")) - public T shiftCloseButtonY(LecternScreen instance, T guiEventListener, Operation original) { - if (guiEventListener instanceof AbstractWidget widget) { - widget.setY(widget.getY() + Scribble.getBookScreenYOffset(height)); - } - - return original.call(instance, guiEventListener); - } -} diff --git a/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java b/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java new file mode 100644 index 0000000..87248c0 --- /dev/null +++ b/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java @@ -0,0 +1,30 @@ +package me.chrr.scribble.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import me.chrr.scribble.screen.ScribbleBookEditScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.WritableBookContent; +import org.jspecify.annotations.NullMarked; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@NullMarked +@Mixin(LocalPlayer.class) +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()) { + // FIXME: this is temporary, maybe a config option? + original.call(instance, screen); + } else { + // FIXME: ideally, I'd like to avoid even constructing the original BookEditScreen. + original.call(instance, new ScribbleBookEditScreen((LocalPlayer) (Object) this, itemStack, hand, book)); + } + } +} diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java new file mode 100644 index 0000000..0e76209 --- /dev/null +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java @@ -0,0 +1,462 @@ +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.book.BookFile; +import me.chrr.scribble.book.FileChooser; +import me.chrr.scribble.book.RichText; +import me.chrr.scribble.config.Config; +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.RichEditBox; +import me.chrr.scribble.gui.edit.RichMultiLineTextField; +import me.chrr.scribble.history.CommandManager; +import me.chrr.scribble.history.HistoryListener; +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.ComponentPath; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.inventory.BookSignScreen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ServerboundEditBookPacket; +import net.minecraft.server.network.Filterable; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.WritableBookContent; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.nio.file.Path; +import java.util.*; + +@NullMarked +public class ScribbleBookEditScreen extends ScribbleBookScreen implements HistoryListener { + private static final ChatFormatting[] COLORS = new ChatFormatting[]{ + ChatFormatting.BLACK, ChatFormatting.DARK_GRAY, + ChatFormatting.GRAY, ChatFormatting.WHITE, + ChatFormatting.DARK_RED, ChatFormatting.RED, + ChatFormatting.GOLD, ChatFormatting.YELLOW, + ChatFormatting.DARK_GREEN, ChatFormatting.GREEN, + ChatFormatting.DARK_AQUA, ChatFormatting.AQUA, + ChatFormatting.DARK_BLUE, ChatFormatting.BLUE, + ChatFormatting.DARK_PURPLE, ChatFormatting.LIGHT_PURPLE, + }; + + private final Player player; + private final ItemStack itemStack; + private final InteractionHand hand; + + private final List pages; + private final CommandManager commandManager = new CommandManager(this); + + private @Nullable RichEditBox lastFocusedEditBox = null; + private boolean dirty = false; + + private @Nullable IconButtonWidget undoButton; + private @Nullable IconButtonWidget redoButton; + + private @Nullable ModifierButtonWidget boldButton; + private @Nullable ModifierButtonWidget italicButton; + private @Nullable ModifierButtonWidget underlineButton; + private @Nullable ModifierButtonWidget strikethroughButton; + private @Nullable ModifierButtonWidget obfuscatedButton; + + private List colorSwatches = List.of(); + + public ScribbleBookEditScreen(Player player, ItemStack itemStack, InteractionHand hand, WritableBookContent book) { + super(Component.translatable("book.edit.title")); + + this.player = player; + this.itemStack = itemStack; + this.hand = hand; + + this.pages = new ArrayList<>(); + book.getPages(Minecraft.getInstance().isTextFilteringEnabled()) + .forEach((page) -> this.pages.add(RichText.fromFormattedString(page))); + + if (this.pages.isEmpty()) { + for (int i = 0; i < this.pagesToShow; i++) { + this.pages.add(RichText.EMPTY); + } + } + } + + //region Widgets (Action, Menu & TextArea) + @Override + protected boolean shouldShowActionButtons() { + return Scribble.config().showActionButtons != Config.ShowActionButtons.NEVER; + } + + @Override + protected void initActionButtons(int x, int y) { + this.undoButton = addRenderableWidget(new IconButtonWidget( + Component.translatable("text.scribble.action.undo"), + () -> { + this.commandManager.tryUndo(); + this.invalidateActionButtons(); + }, + x, y, 24, 90, 12, 12)); + this.redoButton = addRenderableWidget(new IconButtonWidget( + Component.translatable("text.scribble.action.redo"), + () -> { + this.commandManager.tryRedo(); + this.invalidateActionButtons(); + }, + x, y + 12, 36, 90, 12, 12)); + + addRenderableWidget(new IconButtonWidget( + Component.translatable("text.scribble.action.save_book_to_file"), + () -> FileChooser.chooseFile(true, this::saveToFile), + x, y + 12 * 2 + 4, 48, 90, 12, 12)); + addRenderableWidget(new IconButtonWidget( + Component.translatable("text.scribble.action.load_book_from_file"), + () -> this.confirmIf(true, "overwrite_warning", + () -> FileChooser.chooseFile(false, this::loadFromFile)), + x, y + 12 * 3 + 4, 60, 90, 12, 12)); + + this.invalidateActionButtons(); + } + + private void invalidateActionButtons() { + if (undoButton != null && redoButton != null) { + undoButton.active = commandManager.canUndo(); + redoButton.active = commandManager.canRedo(); + } + } + + @Override + protected void initMenuControls(int y) { + this.addRenderableWidget(Button.builder(Component.translatable("book.signButton"), (button) -> { + @SuppressWarnings("DataFlowIssue") + BookSignScreen screen = new BookSignScreen(null, this.player, this.hand, getPagesAsStrings(true)); + ((SetReturnScreen) screen).scribble$setReturnScreen(this); + this.minecraft.setScreen(screen); + }).pos(this.width / 2 - 98 - 2, y).width(98).build()); + + this.addRenderableWidget(Button.builder(CommonComponents.GUI_DONE, (button) -> { + this.minecraft.setScreen(null); + this.saveChanges(); + }).pos(this.width / 2 + 2, y).width(98).build()); + } + + @Override + protected void setInitialFocus() { + super.setInitialFocus(this.textAreas.getFirst()); + } + + @Override + protected TextArea createTextArea(int x, int y, int width, int height, int pageOffset) { + RichEditBox editBox = (RichEditBox) new RichEditBox.Builder() + .onHistoryPush((command) -> this.pushCommand(pageOffset, command)) + .onInvalidateFormat(this::invalidateFormattingButtons) + .setShowDecorations(false) + .setTextColor(0xff000000).setCursorColor(0xff000000) + .setShowBackground(false).setTextShadow(false) + .setX(x - 4).setY(y - 4) + .build(this.font, width + 8, height + 6, CommonComponents.EMPTY); + + editBox.setCharacterLimit(1024); + editBox.setLineLimit(height / this.font.lineHeight); + + editBox.setRichValueListener((text) -> { + RichText existing = this.pages.get(this.currentPage + pageOffset); + if (existing != text) { + this.pages.set(this.currentPage + pageOffset, text); + this.dirty = true; + } + }); + + return editBox; + } + + private void updateFocusedEditBox() { + if (this.getFocused() instanceof RichEditBox focusedEditBox && this.lastFocusedEditBox != focusedEditBox) { + this.lastFocusedEditBox = focusedEditBox; + this.textAreas.stream().map((textArea) -> (RichEditBox) textArea) + .filter((editBox) -> !editBox.isFocused()) + .forEach((editBox) -> editBox.getRichTextField().resetCursor(false)); + focusedEditBox.getRichTextField().sendUpdateFormat(); + } + } + + @Override + protected void changeFocus(ComponentPath componentPath) { + super.changeFocus(componentPath); + this.updateFocusedEditBox(); + } + + @Override + public void setFocused(@Nullable GuiEventListener guiEventListener) { + super.setFocused(guiEventListener); + this.updateFocusedEditBox(); + } + //endregion + + //region Saving and Loading + public void confirmIf(boolean condition, String name, Runnable runnable) { + if (!condition) { + runnable.run(); + return; + } + + BooleanConsumer onConfirmed = (confirmed) -> { + this.minecraft.setScreen(this); + if (confirmed) runnable.run(); + }; + + this.minecraft.setScreen(new ConfirmScreen( + onConfirmed, + Component.translatable("text.scribble." + name + ".title"), + Component.translatable("text.scribble." + name + ".description") + )); + } + + private void saveToFile(Path path) { + try { + BookFile bookFile = new BookFile(this.player.getName().getString(), this.getPagesAsStrings(true)); + bookFile.writeJson(path); + } catch (Exception e) { + Scribble.LOGGER.error("could not save book to file", e); + } + } + + private void loadFromFile(Path path) { + try { + BookFile bookFile = BookFile.readFile(path); + + this.pages.clear(); + this.pages.addAll(bookFile.pages().stream().map(RichText::fromFormattedString).toList()); + this.commandManager.clear(); + this.dirty = true; + + this.showPage(0, false); + this.updateCurrentPages(); + } catch (Exception e) { + Scribble.LOGGER.error("could not load book from file", e); + } + } + + @Override + public void onClose() { + this.confirmIf(this.dirty, "quit_without_saving", super::onClose); + } + //endregion + + //region Hotkeys + @Override + public boolean keyPressed(KeyEvent keyEvent) { + if (keyEvent.hasControlDown() && !keyEvent.hasAltDown()) { + // On Ctrl-Z, undo. + if ((KeyboardUtil.isKey(keyEvent.key(), "Z") && !keyEvent.hasShiftDown() + && undoButton != null && undoButton.active)) { + undoButton.onPress(keyEvent); + return true; + } + + // On Ctrl-Shift-Z or Ctrl-Y, redo. + if (((KeyboardUtil.isKey(keyEvent.key(), "Z") && keyEvent.hasShiftDown()) + || (KeyboardUtil.isKey(keyEvent.key(), "Y") && !keyEvent.hasShiftDown())) + && redoButton != null && redoButton.active) { + redoButton.onPress(keyEvent); + return true; + } + } + + return super.keyPressed(keyEvent); + } + //endregion + + //region Formatting + @Override + protected void init() { + super.init(); + + int x = this.width / 2 + this.getBackgroundWidth() / 2 - 20; + int y = this.getBackgroundY() + 12; + + // Modifier buttons + boldButton = addRenderableWidget(new ModifierButtonWidget( + Component.translatable("text.scribble.modifier.bold"), + (toggled) -> this.applyFormat(ChatFormatting.BOLD, toggled), + x, y, 0, 0, 22, 19)); + italicButton = addRenderableWidget(new ModifierButtonWidget( + Component.translatable("text.scribble.modifier.italic"), + (toggled) -> this.applyFormat(ChatFormatting.ITALIC, toggled), + x, y + 19, 0, 19, 22, 17)); + underlineButton = addRenderableWidget(new ModifierButtonWidget( + Component.translatable("text.scribble.modifier.underline"), + (toggled) -> this.applyFormat(ChatFormatting.UNDERLINE, toggled), + x, y + 36, 0, 36, 22, 17)); + strikethroughButton = addRenderableWidget(new ModifierButtonWidget( + Component.translatable("text.scribble.modifier.strikethrough"), + (toggled) -> this.applyFormat(ChatFormatting.STRIKETHROUGH, toggled), + x, y + 53, 0, 53, 22, 17)); + obfuscatedButton = addRenderableWidget(new ModifierButtonWidget( + Component.translatable("text.scribble.modifier.obfuscated"), + (toggled) -> this.applyFormat(ChatFormatting.OBFUSCATED, toggled), + x, y + 70, 0, 70, 22, 18)); + + // Color swatches + colorSwatches = new ArrayList<>(COLORS.length); + for (int i = 0; i < COLORS.length; i++) { + int dx = (i % 2) * 8; + int dy = (i / 2) * 8; + + ChatFormatting color = COLORS[i]; + colorSwatches.add(addRenderableWidget(new ColorSwatchWidget( + Component.translatable("text.scribble.color." + color.getName()), color, + () -> this.applyFormat(color, true), + x + 3 + dx, y + 95 + dy, 8, 8 + ))); + } + + this.invalidateFormattingButtons(); + } + + private void applyFormat(ChatFormatting formatting, boolean enabled) { + if (this.lastFocusedEditBox == null) + return; + this.lastFocusedEditBox.applyFormat(formatting, enabled); + } + + private void setSwatchColor(@Nullable ChatFormatting color) { + for (ColorSwatchWidget swatch : colorSwatches) { + swatch.setToggled(swatch.getColor() == color); + } + } + + private void invalidateFormattingButtons() { + RichEditBox editBox = this.lastFocusedEditBox; + if (editBox == null) + return; + + // Sometimes, these buttons get invalidated on screen initialize, when the buttons don't exist yet. + // We can just return in that case. + if (boldButton == null || italicButton == null || underlineButton == null + || strikethroughButton == null || obfuscatedButton == null) + return; + + boldButton.toggled = editBox.modifiers.contains(ChatFormatting.BOLD); + italicButton.toggled = editBox.modifiers.contains(ChatFormatting.ITALIC); + underlineButton.toggled = editBox.modifiers.contains(ChatFormatting.UNDERLINE); + strikethroughButton.toggled = editBox.modifiers.contains(ChatFormatting.STRIKETHROUGH); + obfuscatedButton.toggled = editBox.modifiers.contains(ChatFormatting.OBFUSCATED); + + setSwatchColor(editBox.color); + } + //endregion + + //region Page Management + @Override + protected RichText getPage(int page) { + return this.pages.get(page); + } + + @Override + protected int getTotalPages() { + return this.pages.size(); + } + + @Override + public void showPage(int page, boolean insertIfMissing) { + super.showPage(page, insertIfMissing); + this.setFocused(this.textAreas.get(Math.min(page, this.getTotalPages()) % this.textAreas.size())); + } + + private List getPagesAsStrings(boolean removeTrailingPages) { + List pages = new ArrayList<>(this.pages); + + if (removeTrailingPages) { + ListIterator listIterator = pages.listIterator(pages.size()); + while (listIterator.hasPrevious() && listIterator.previous().isEmpty()) { + listIterator.remove(); + } + } + + return pages.stream().map(RichText::getAsFormattedString).toList(); + } + + private void saveChanges() { + // Update local copy. + List pages = getPagesAsStrings(true); + this.itemStack.set(DataComponents.WRITABLE_BOOK_CONTENT, new WritableBookContent(pages.stream().map(Filterable::passThrough).toList())); + + // Update remote copy. + int slot = this.hand == InteractionHand.MAIN_HAND ? this.player.getInventory().getSelectedSlot() : Inventory.SLOT_OFFHAND; + ClientPacketListener connection = this.minecraft.getConnection(); + Objects.requireNonNull(connection).send(new ServerboundEditBookPacket(slot, pages, Optional.empty())); + } + + @Override + protected boolean canInsertPages() { + return this.getTotalPages() < 100; + } + + @Override + protected void insertEmptyPageAt(int page) { + this.pages.add(page, RichText.EMPTY); + } + //endregion + + //region History + public void pushCommand(int pageOffset, Command command) { + if (command instanceof EditCommand editCommand) { + editCommand.page = this.currentPage + pageOffset; + } + + this.commandManager.push(command); + this.dirty = true; + this.invalidateActionButtons(); + } + + @Override + public RichMultiLineTextField switchAndFocusPage(int page) { + this.showPage(page, false); + int shownPage = Math.min(page, this.getTotalPages()); + + RichEditBox editBox = (RichEditBox) this.textAreas.get(shownPage % this.pagesToShow); + setFocused(editBox); + + return editBox.getRichTextField(); + } + + @Override + public void setFormat(@Nullable ChatFormatting color, Set modifiers) { + RichEditBox editBox = this.lastFocusedEditBox; + if (editBox == null) + return; + + editBox.color = color; + editBox.modifiers = modifiers; + this.invalidateFormattingButtons(); + } + + @Override + public void insertPageAt(int page, @Nullable RichText content) { + this.pages.add(page, Optional.ofNullable(content).orElse(RichText.EMPTY)); + this.dirty = true; + this.updateCurrentPages(); + } + + @Override + public void deletePage(int page) { + this.pages.remove(page); + this.dirty = true; + this.updateCurrentPages(); + } + //endregion +} diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java new file mode 100644 index 0000000..0d9b8fc --- /dev/null +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java @@ -0,0 +1,233 @@ +package me.chrr.scribble.screen; + +import me.chrr.scribble.Scribble; +import me.chrr.scribble.gui.PageNumberWidget; +import me.chrr.scribble.gui.TextArea; +import me.chrr.scribble.gui.button.IconButtonWidget; +import net.minecraft.ChatFormatting; +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; +import net.minecraft.client.gui.screens.inventory.PageButton; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.resources.Identifier; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +@NullMarked +public abstract class ScribbleBookScreen extends Screen { + public int currentPage = 0; + public int pagesToShow = 1; + + public Identifier backgroundTexture = BookViewScreen.BOOK_LOCATION; + + public final List> textAreas = new ArrayList<>(); + public final List pageNumbers = new ArrayList<>(); + + public @Nullable PageButton backButton; + public @Nullable PageButton forwardButton; + + protected ScribbleBookScreen(Component title) { + super(title); + } + + //region Widgets + @Override + protected void init() { + this.pagesToShow = Scribble.config().pagesToShow; + if (this.pagesToShow == 1) { + this.backgroundTexture = BookViewScreen.BOOK_LOCATION; + } else { + this.backgroundTexture = Scribble.id("textures/gui/book_" + this.pagesToShow + ".png"); + } + + int x = this.getBackgroundX(); + int y = this.getBackgroundY(); + + this.textAreas.clear(); + for (int i = 0; i < this.pagesToShow; i++) { + TextArea textArea = createTextArea(x + 36 + i * 126, y + 30, 114, 128, i); + this.textAreas.add(addRenderableWidget(textArea)); + } + + this.pageNumbers.clear(); + for (int i = 0; i < this.pagesToShow; i++) { + PageNumberWidget widget = new PageNumberWidget(page -> showPage(page - 1, false), x + 148 + i * 126, y + 16, this.font); + this.pageNumbers.add(addRenderableWidget(widget)); + } + + initMenuControls(getMenuControlsY()); + + this.backButton = addRenderableWidget(new PageButton(x + 43, y + 157, false, + (button) -> this.goPageBackward(this.minecraft.hasShiftDown()), true)); + this.forwardButton = addRenderableWidget(new PageButton(x + 126 * this.pagesToShow - 10, y + 157, true, + (button) -> this.goPageForward(this.minecraft.hasShiftDown()), true)); + + if (shouldShowActionButtons()) { + initActionButtons(x, this.getBackgroundY() + 12 + 4); + initSettingsButton(x, this.getBackgroundY() + this.getBackgroundHeight() - 12 - 4 - 12); + } + + this.updateCurrentPages(); + } + + private void initSettingsButton(int x, int y) { + MutableComponent settingsText = Component.literal("Scribble " + Scribble.platform().VERSION + "\n") + .setStyle(Style.EMPTY.withBold(true)); + + boolean canOpenConfigScreen = Scribble.platform().HAS_YACL; + if (canOpenConfigScreen) { + settingsText.append(Component.translatable("text.scribble.action.settings") + .setStyle(Style.EMPTY.withBold(false))); + } else { + // FIXME: make this a translatable string + settingsText.append(Component.literal("YACL needs to be installed to access the settings menu.") + .setStyle(Style.EMPTY.withBold(false).withColor(ChatFormatting.RED))); + } + + IconButtonWidget widget = addRenderableWidget(new IconButtonWidget( + settingsText, + () -> minecraft.setScreen(Scribble.buildConfigScreen(this)), + x, y, 96, 90, 12, 12)); + widget.active = canOpenConfigScreen; + } + + public void updateCurrentPages() { + // Insert pages so every visible page exists. + int pagesFromEnd = this.getTotalPages() - this.currentPage; + while (pagesFromEnd < this.pagesToShow && this.canInsertPages()) { + this.insertEmptyPageAt(this.getTotalPages()); + pagesFromEnd += 1; + } + + // Switch the text areas and page numbers. + for (int i = 0; i < this.pagesToShow; i++) { + boolean visible = i < pagesFromEnd; + this.textAreas.get(i).setVisible(visible); + this.pageNumbers.get(i).visible = visible; + + if (visible) { + this.textAreas.get(i).setText(this.getPage(this.currentPage + i)); + this.pageNumbers.get(i).setPageNumber(this.currentPage + i + 1, Math.max(1, this.getTotalPages())); + } + } + + // Show / hide the page switch buttons if necessary. + if (this.backButton != null && this.forwardButton != null) { + this.backButton.visible = this.currentPage > 0; + this.forwardButton.visible = pagesFromEnd > this.pagesToShow || this.canInsertPages(); + } + } + + public void showPage(int page, boolean insertIfMissing) { + // Insert pages so the requested page exists if needed. + int newPage = Math.clamp(page, 0, this.getTotalPages() - 1); + if (insertIfMissing) { + while (newPage < page && this.canInsertPages()) { + this.insertEmptyPageAt(this.getTotalPages()); + newPage += 1; + } + } + + // Show the left most page, so the page numbers don't get offset. + int shownPage = newPage - newPage % this.pagesToShow; + if (shownPage != this.currentPage) { + this.currentPage = shownPage; + this.updateCurrentPages(); + + // When switching to a new page, always focus the left most page. + if (this.textAreas.stream().anyMatch(GuiEventListener::isFocused)) + this.setFocused(this.textAreas.getFirst()); + } + } + + public void goPageForward(boolean toEnd) { + if (toEnd) { + showPage(this.getTotalPages(), false); + } else { + showPage(this.currentPage + this.pagesToShow, true); + } + } + + public void goPageBackward(boolean toStart) { + if (toStart) { + showPage(0, false); + } else { + showPage(this.currentPage - 1, false); + } + } + //endregion + + //region Rendering and dimensions + @Override + public void renderBackground(GuiGraphics guiGraphics, int i, int j, float f) { + super.renderBackground(guiGraphics, i, j, f); + + int textureSize = this.pagesToShow == 1 ? 256 : 512; + guiGraphics.blit(RenderPipelines.GUI_TEXTURED, backgroundTexture, this.getBackgroundX(), this.getBackgroundY(), + 0.0F, 0.0F, 122 * this.pagesToShow + 70, 192, textureSize, textureSize); + } + + @Override + public boolean isInGameUi() { + return true; + } + + public int getBackgroundX() { + return this.width / 2 - getBackgroundWidth() / 2; + } + + public int getBackgroundY() { + if (Scribble.config().centerBookGui) { + // Perfect centering actually doesn't look great, so we put it on a third. + return this.height / 3 - this.getMenuHeight() / 3; + } else { + return 2; + } + } + + public int getBackgroundWidth() { + return this.pagesToShow * 126 + 66; + } + + public int getBackgroundHeight() { + return 182; + } + + public int getMenuHeight() { + return 192 + 2 + 20; + } + + public int getMenuControlsY() { + return this.getBackgroundY() + getMenuHeight() - 20; + } + //endregion + + //region Abstract methods + protected boolean canInsertPages() { + return false; + } + + protected void insertEmptyPageAt(int page) { + } + + protected abstract boolean shouldShowActionButtons(); + + protected abstract void initActionButtons(int x, int y); + + protected abstract void initMenuControls(int y); + + protected abstract TextArea createTextArea(int x, int y, int width, int height, int pageOffset); + + protected abstract T getPage(int page); + + protected abstract int getTotalPages(); + //endregion +} diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java new file mode 100644 index 0000000..3ed4c77 --- /dev/null +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java @@ -0,0 +1,90 @@ +package me.chrr.scribble.screen; + +import me.chrr.scribble.Scribble; +import me.chrr.scribble.book.BookFile; +import me.chrr.scribble.book.FileChooser; +import me.chrr.scribble.book.RichText; +import me.chrr.scribble.config.Config; +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.Screen; +import net.minecraft.client.gui.screens.inventory.BookViewScreen; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.NullMarked; + +import java.util.List; +import java.util.Objects; + +@NullMarked +public class ScribbleBookViewScreen extends ScribbleBookScreen { + private final BookViewScreen.BookAccess book; + + public ScribbleBookViewScreen(BookViewScreen.BookAccess book) { + super(Component.translatable("book.view.title")); + this.book = book; + } + + @Override + protected boolean shouldShowActionButtons() { + return Scribble.config().showActionButtons == Config.ShowActionButtons.ALWAYS; + } + + @Override + protected void initActionButtons(int x, int y) { + addRenderableWidget(new IconButtonWidget( + Component.translatable("text.scribble.action.save_book_to_file"), + this::saveBookToFile, + x, y, 48, 90, 12, 12)); + } + + @Override + protected void initMenuControls(int y) { + this.addRenderableWidget(Button.builder(CommonComponents.GUI_DONE, (button) -> this.onClose()) + .pos((this.width - 200) / 2, y).width(200).build()); + } + + @Override + protected TextArea createTextArea(int x, int y, int width, int height, int pageOffset) { + return new BookTextWidget(x, y, width, height, this.font, this::handleClickEvent); + } + + @Override + protected Component getPage(int page) { + return book.getPage(page); + } + + @Override + protected int getTotalPages() { + return book.getPageCount(); + } + + private void saveBookToFile() { + FileChooser.chooseFile(true, (path) -> { + try { + List pages = this.book.pages().stream() + .map(RichText::fromFormattedTextLossy) + .map(RichText::getAsFormattedString) + .toList(); + + BookFile bookFile = new BookFile("", pages); + bookFile.writeJson(path); + } catch (Exception e) { + Scribble.LOGGER.error("could not save book to file", e); + } + }); + } + + private void handleClickEvent(ClickEvent event) { + switch (event) { + case ClickEvent.ChangePage(int page) -> this.showPage(page - 1, false); + // FIXME: closeContainerOnServer for lecterns. + case ClickEvent.RunCommand(String command) -> + clickCommandAction(Objects.requireNonNull(this.minecraft.player), command, null); + default -> Screen.defaultHandleGameClickEvent(event, this.minecraft, this); + } + } +} diff --git a/src/main/resources/assets/scribble/lang/en_us.json b/src/main/resources/assets/scribble/lang/en_us.json index 6f98ccf..5c9eb1f 100644 --- a/src/main/resources/assets/scribble/lang/en_us.json +++ b/src/main/resources/assets/scribble/lang/en_us.json @@ -14,6 +14,7 @@ "text.scribble.action.redo": "Redo", "text.scribble.action.save_book_to_file": "Save book to file...", "text.scribble.action.load_book_from_file": "Load book from file...", + "text.scribble.action.settings": "Click to open settings...", "text.scribble.overwrite_warning.title": "Are you sure you want to overwrite this book?", "text.scribble.overwrite_warning.description": "The current contents will be lost forever! (A long time!)", "text.scribble.quit_without_saving.title": "Are you sure you want to quit without saving?", diff --git a/src/main/resources/assets/scribble/textures/gui/book_2.png b/src/main/resources/assets/scribble/textures/gui/book_2.png new file mode 100644 index 0000000000000000000000000000000000000000..662f8046e614f2f95e27ee5b385af47ba71a17ab GIT binary patch literal 11053 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/scribble/textures/gui/scribble_widgets.png b/src/main/resources/assets/scribble/textures/gui/scribble_widgets.png index 80ab97a4f40d9520b052b5304cc833bdac104f9d..16a81a7fba740185900e167d47ffe4dd313de425 100644 GIT binary patch delta 1921 zcmV-{2Y&d>DVPtC85IKn008vhk@)}s00DDSM?wMF$t-`7As2rDT2M?>MNAqC00016 zBNBBo9{>OUWhfTRxtODndGqJU;L@)8@Yce%pR=Tj$i1rl`0D@q?#jix%*Va}0080B zx5coL`R~)o$;ml6Io(Vh<%(j?9{}ujK6Z9?R#sNzbk+p`0016zQchF<0RR90|NsC0 z|NsC0|NsC0|Nnpg|NsC0|NsC0|NmB7?fU=#2CYd%K~#90?VD+D<2De6M~8#-t2|~nhp4P2r}hdP7VA*2Y0kspF{GpxAX7n^@oM$bRFv{3J*h=0 z)GnzD%3cmoiLzK}?c#=o7!ax$11YI7 zAk@WUKuHNes|29b1fbQW07i=dlcEv8inC>x0NmXJ_a6ga0Mb-Dwi$qS>@on|_?`ji z#(!4<)N#=@07+Eyr%AY32- zgbM_KaKR=4tbKSdWf!n2y8tc0TP?eQ)lCA_bly||LKOfdomdrsP*-_?lJ)?rY8F7w z1FT*HaKDAf|XG3MGeaEw*Ru|ZhcSs&404W$M zfD{ZBKnjK%2S}0soZA#@fE4-L+@^Rf0Ht#-7#aXdZ7vwD1Hf?B>HUaQc|U@&XVCi* zsVWLo@|wVaxzHH3QJWm;rF=QUHIWA%Ia4z^Djd)P(?63wKJVMa?4{6z9q= z0l*5-<>_c>ROJD5c{&;z^{PSG=Ei_Mn}Pu_>ZWJG%>gDmA{CA`z+^|H!tu2LejrWu z145Mmet=H)143Q9ADDVTsAfYl^?*>q3(ZvzEVR1nc0F%D)f1D>0))srSOBfAdtNXZ14dPQGCKy0y1n}Y z>fub814z?QKb)y^0L!Q0{Q>y0cU?KTLlV@<9g?DTLu7J?B&eIaLy{`+PUk_WRqFxo zbRL9S-S#ZF8vq49vk)cK9uB7ftZsi6pq@UmVbuVX(?>R}t^}xau^_1$pv=XBq^<*i z+yfICt#CljH{2Nu}d`eY*1O+T>sB>`yr*~S+l5x}YufVQ8V(;>!V zu_+~WApl)a{A<-aos{X+4FZaP@9LdSQkMbn1^(4{C@2@+<(jDa4h4OG`M!Txy(vpS zA9|^#>bpf{F9wholo>u(y%^vgDFQsg>m30eA-po@86Hu+Ax=^3M>^NO!amGms6Q8= z;`hLF0Iv-f^P4AceH{;0|}ryfnl z|G0SxU=ODxCzu9K^8_BC7~FqWqN}Y3aHh)&<3)3FC6Cc)wLk4o2ou^*TzmI%l0LW) z0Pv9i>h1VSVl7q7qDvZ|iIy4%(j*u-CEy-JpEak80fsOy&%p=LW*! zu)!A5-;j;zX!7CWs0y*sKmN|!4=Z8l-_Yhgs;8OK>r*r1fS)B6e1jMJMKvvZb;^xH z2k^~DQ8x}=JV;D0Y89;Dg1o5wk}?9|X`n^s1)kz~96uL;V!;dmJmI0%S$*<%gP+8MF8Zg7q`SyEO6hZSP&}2SP)<^76br{ z1r@cnB7hhRF7rV6AFPJa0i;+^VT|)#f)yTl3}C+JSTF^Mv0#550HV#{0Urzc62AM= z7zPkaPAi7xJIH5Xk^O0m!BsF2(mvzXZSm;CUUu;rXM>1pvi3i|`S^p*81%-8zB> zcJl-u0370<=jma^;^cj>9H7~SBU5PhA!ggTo!t*ovs(jzEC z**Wje?ri@z0LY#T)}-d)Jby5`N&54znJ;by@UtIG?wI}qkqhfv^+iqS00000NkvXX Hu0mjfGF5j! literal 5324 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 MW^I~peBk)M0Mo|T8vp Date: Tue, 23 Dec 2025 15:46:19 +0100 Subject: [PATCH 02/16] feat: use Parchment mappings --- build.gradle.kts | 9 ++++++++- gradle.properties | 1 + versions/1.21.11/gradle.properties | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index bb4df55..bd78c72 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,11 +23,18 @@ architectury.common(stonecutter.tree.branches.mapNotNull { repositories { maven("https://maven.shedaniel.me/") maven("https://maven.isxander.dev/releases") + maven("https://maven.parchmentmc.org") } dependencies { minecraft("com.mojang:minecraft:$minecraft") - mappings(loom.officialMojangMappings()) + + @Suppress("UnstableApiUsage") + mappings(loom.layered { + officialMojangMappings() + parchment("org.parchmentmc.data:parchment-${prop("parchment", "version")}@zip") + }) + modImplementation("net.fabricmc:fabric-loader:${prop("fabric", "loaderVersion")}") modCompileOnly("me.shedaniel.cloth:cloth-config-fabric:${prop("clothconfig", "version")}") diff --git a/gradle.properties b/gradle.properties index a5d7372..e1489db 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,7 @@ org.gradle.jvmargs=-Xmx1G platform.versions=[versioned] +parchment.version=[versioned] # Mod details mod.name=Scribble diff --git a/versions/1.21.11/gradle.properties b/versions/1.21.11/gradle.properties index c7b9e14..74fb99d 100644 --- a/versions/1.21.11/gradle.properties +++ b/versions/1.21.11/gradle.properties @@ -1,5 +1,6 @@ platform.versions=1.21.11 minecraft.version=1.21.11 +parchment.version=1.21.11:2025.12.20 mod.accesswidener=1.21.7 From 6373787c7a3b8e6ad78a300a706b1ffe47bdb1b5 Mon Sep 17 00:00:00 2001 From: chrrrs Date: Tue, 23 Dec 2025 18:07:38 +0100 Subject: [PATCH 03/16] feat: add back page insert/delete button --- .../screen/ScribbleBookEditScreen.java | 47 +++++++++++++++++++ .../scribble/screen/ScribbleBookScreen.java | 4 ++ .../resources/assets/scribble/lang/en_us.json | 9 ++-- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java index 0e76209..7daf8b9 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java @@ -18,6 +18,8 @@ import me.chrr.scribble.history.HistoryListener; import me.chrr.scribble.history.command.Command; import me.chrr.scribble.history.command.EditCommand; +import me.chrr.scribble.history.command.PageDeleteCommand; +import me.chrr.scribble.history.command.PageInsertCommand; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.ComponentPath; @@ -69,6 +71,8 @@ public class ScribbleBookEditScreen extends ScribbleBookScreen impleme private @Nullable IconButtonWidget undoButton; private @Nullable IconButtonWidget redoButton; + private final List insertPageButtons = new ArrayList<>(); + private @Nullable ModifierButtonWidget boldButton; private @Nullable ModifierButtonWidget italicButton; private @Nullable ModifierButtonWidget underlineButton; @@ -138,6 +142,48 @@ private void invalidateActionButtons() { } } + @Override + protected void initPageButtons(int y) { + this.insertPageButtons.clear(); + + for (int i = 0; i < this.pagesToShow; i++) { + int xOffset = this.pagesToShow == 1 + ? 0 : i == 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'. + Component insertText = this.pagesToShow == 1 + ? Component.translatable("text.scribble.action.insert_new_page") + : Component.translatable("text.scribble.action.insert_new_page_here"); + Component deleteText = Component.translatable("text.scribble.action.delete_page"); + + int pageOffset = i; + this.insertPageButtons.add(addRenderableWidget(new IconButtonWidget(insertText, + () -> { + PageInsertCommand command = new PageInsertCommand(this.currentPage + pageOffset); + command.execute(this); + commandManager.push(command); + }, + getBackgroundX() + 78 + xOffset + i * 126, y + 2, 12, 90, 12, 12))); + addRenderableWidget(new IconButtonWidget(deleteText, + () -> { + PageDeleteCommand command = new PageDeleteCommand(this.currentPage + pageOffset, + this.pages.get(this.currentPage + pageOffset)); + command.execute(this); + commandManager.push(command); + }, + getBackgroundX() + 94 + xOffset + i * 126, y + 2, 0, 90, 12, 12)); + } + } + + @Override + public void updateCurrentPages() { + super.updateCurrentPages(); + this.insertPageButtons.forEach((button) -> button.visible = this.getTotalPages() < 100); + } + @Override protected void initMenuControls(int y) { this.addRenderableWidget(Button.builder(Component.translatable("book.signButton"), (button) -> { @@ -449,6 +495,7 @@ public void setFormat(@Nullable ChatFormatting color, Set modifi public void insertPageAt(int page, @Nullable RichText content) { this.pages.add(page, Optional.ofNullable(content).orElse(RichText.EMPTY)); this.dirty = true; + this.showPage(page, false); this.updateCurrentPages(); } diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java index 0d9b8fc..63ad19f 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java @@ -67,6 +67,7 @@ protected void init() { this.backButton = addRenderableWidget(new PageButton(x + 43, y + 157, false, (button) -> this.goPageBackward(this.minecraft.hasShiftDown()), true)); + initPageButtons(y + 157); this.forwardButton = addRenderableWidget(new PageButton(x + 126 * this.pagesToShow - 10, y + 157, true, (button) -> this.goPageForward(this.minecraft.hasShiftDown()), true)); @@ -222,6 +223,9 @@ protected void insertEmptyPageAt(int page) { protected abstract void initActionButtons(int x, int y); + protected void initPageButtons(int y) { + } + protected abstract void initMenuControls(int y); protected abstract TextArea createTextArea(int x, int y, int width, int height, int pageOffset); diff --git a/src/main/resources/assets/scribble/lang/en_us.json b/src/main/resources/assets/scribble/lang/en_us.json index 5c9eb1f..b3f2298 100644 --- a/src/main/resources/assets/scribble/lang/en_us.json +++ b/src/main/resources/assets/scribble/lang/en_us.json @@ -1,5 +1,5 @@ { - "config.scribble.title": "Scribble Config", + "config.scribble.title": "Scribble Settings", "config.scribble.option.copy_formatting_codes": "Copy formatting codes", "config.scribble.description.copy_formatting_codes": "This option can be temporarily disabled by\npressing SHIFT while copying or pasting text.", "config.scribble.option.center_book_gui": "Vertically center book GUI's", @@ -8,15 +8,16 @@ "config.scribble.option.show_action_buttons.when_editing": "When editing", "config.scribble.option.show_action_buttons.never": "Never", "config.scribble.option.edit_history_size": "Undo/redo history size", - "text.scribble.action.delete_page": "Delete page", - "text.scribble.action.insert_new_page": "Insert new page\nbefore current", + "text.scribble.action.delete_page": "Delete this page", + "text.scribble.action.insert_new_page": "Insert new page\nbefore this page", + "text.scribble.action.insert_new_page_here": "Insert new page here", "text.scribble.action.undo": "Undo", "text.scribble.action.redo": "Redo", "text.scribble.action.save_book_to_file": "Save book to file...", "text.scribble.action.load_book_from_file": "Load book from file...", "text.scribble.action.settings": "Click to open settings...", "text.scribble.overwrite_warning.title": "Are you sure you want to overwrite this book?", - "text.scribble.overwrite_warning.description": "The current contents will be lost forever! (A long time!)", + "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.modifier.bold": "Bold", From 9e0db40072890fadfd24c96a630f9675ca298490 Mon Sep 17 00:00:00 2001 From: chrrrs Date: Wed, 24 Dec 2025 14:27:06 +0100 Subject: [PATCH 04/16] feat: center book sign screen --- .../scribble/mixin/BookSignScreenMixin.java | 61 ++++++++++++++++++- .../scribble/screen/ScribbleBookScreen.java | 6 +- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java b/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java index 20113c0..7942fce 100644 --- a/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java +++ b/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java @@ -2,19 +2,35 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import me.chrr.scribble.Scribble; 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.components.AbstractWidget; +import net.minecraft.client.gui.components.Renderable; +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.gui.screens.inventory.BookSignScreen; +import net.minecraft.network.chat.Component; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @NullMarked @Mixin(BookSignScreen.class) -public abstract class BookSignScreenMixin implements SetReturnScreen { +public abstract class BookSignScreenMixin extends Screen implements SetReturnScreen { + private BookSignScreenMixin(Component title) { + super(title); + } + + //region Return Screen @Unique public @Nullable Screen scribble$returnScreen = null; @@ -27,4 +43,47 @@ public abstract class BookSignScreenMixin implements SetReturnScreen { public void redirectReturnScreen(Minecraft instance, Screen screen, Operation original) { original.call(instance, this.scribble$returnScreen != null ? this.scribble$returnScreen : screen); } + //endregion + + //region Centering + // If we need to center the GUI, we shift the Y of the texture draw call down. + @ModifyArg(method = "renderBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;blit(Lcom/mojang/blaze3d/pipeline/RenderPipeline;Lnet/minecraft/resources/Identifier;IIFFIIII)V"), index = 3) + public int shiftBackgroundY(int y) { + return scribble$getYOffset() + y; + } + + // If we need to center the GUI, we shift the Y of the close buttons down. + @WrapOperation(method = "init", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/BookSignScreen;addRenderableWidget(Lnet/minecraft/client/gui/components/events/GuiEventListener;)Lnet/minecraft/client/gui/components/events/GuiEventListener;")) + public T shiftButtonY(BookSignScreen instance, T guiEventListener, Operation original) { + if (guiEventListener instanceof AbstractWidget widget) { + widget.setY(widget.getY() + scribble$getYOffset()); + } + + return original.call(instance, guiEventListener); + } + + // 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) { + 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) { + graphics.pose().popMatrix(); + } + + @Unique + private int scribble$getYOffset() { + if (Scribble.config().centerBookGui) { + // See ScribbleBookScreen#getBackgroundY(). + return this.height / 3 - ScribbleBookScreen.getMenuHeight() / 3; + } else { + return 0; + } + } + //endregion } diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java index 63ad19f..c36c0d5 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java @@ -188,7 +188,7 @@ public int getBackgroundX() { public int getBackgroundY() { if (Scribble.config().centerBookGui) { // Perfect centering actually doesn't look great, so we put it on a third. - return this.height / 3 - this.getMenuHeight() / 3; + return 2 + this.height / 3 - getMenuHeight() / 3; } else { return 2; } @@ -202,8 +202,8 @@ public int getBackgroundHeight() { return 182; } - public int getMenuHeight() { - return 192 + 2 + 20; + public static int getMenuHeight() { + return 194 + 20; } public int getMenuControlsY() { From bcead2237da4be544cd56c0fac695b258058c3c1 Mon Sep 17 00:00:00 2001 From: chrrrs Date: Wed, 24 Dec 2025 14:51:14 +0100 Subject: [PATCH 05/16] fix: add a config option to open vanilla GUI's on SHIFT --- .../java/me/chrr/scribble/fabric/ModMenuCompat.java | 3 ++- src/main/java/me/chrr/scribble/config/Config.java | 1 + .../scribble/config/YACLConfigScreenFactory.java | 13 ++++++++++++- .../scribble/mixin/ClientPacketListenerMixin.java | 6 +++--- .../me/chrr/scribble/mixin/LocalPlayerMixin.java | 4 ++-- 5 files changed, 20 insertions(+), 7 deletions(-) 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 10c0e77..9f6fb99 100644 --- a/fabric/src/main/java/me/chrr/scribble/fabric/ModMenuCompat.java +++ b/fabric/src/main/java/me/chrr/scribble/fabric/ModMenuCompat.java @@ -3,6 +3,7 @@ import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; import me.chrr.scribble.Scribble; +import net.fabricmc.loader.api.FabricLoader; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -10,7 +11,7 @@ public class ModMenuCompat implements ModMenuApi { @Override public @Nullable ConfigScreenFactory getModConfigScreenFactory() { - if (Scribble.platform().HAS_YACL) { + if (FabricLoader.getInstance().isModLoaded("yet_another_config_lib_v3")) { return Scribble::buildConfigScreen; } diff --git a/src/main/java/me/chrr/scribble/config/Config.java b/src/main/java/me/chrr/scribble/config/Config.java index 74404d8..586ab13 100644 --- a/src/main/java/me/chrr/scribble/config/Config.java +++ b/src/main/java/me/chrr/scribble/config/Config.java @@ -12,6 +12,7 @@ public class Config { public ShowActionButtons showActionButtons = ShowActionButtons.WHEN_EDITING; public int editHistorySize = 32; public int pagesToShow = 1; + public boolean openVanillaBookScreenOnShift = false; @DeprecatedConfigOption private boolean showSaveLoadButtons = true; diff --git a/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java b/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java index ea7b2b9..3f5e3e6 100644 --- a/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java +++ b/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java @@ -57,7 +57,7 @@ public static Screen create(ConfigManager configManager, Screen parent) { .description(OptionDescription.of(Component.literal("These options affect how you can write books."))) .option(Option.createBuilder() .name(Component.literal("Copy formatting codes")) - .description(OptionDescription.of(Component.literal("When copying formatted text, this option determines whether formatting codes (&) should be copied to your clipboard. This allows you to paste text back into the book using the copied formatting.\n\nNote: This option can be temporarily disabled by holding SHIFT while copying or pasting text."))) + .description(OptionDescription.of(Component.literal("When copying formatted text, this option determines whether formatting codes (&) should be copied to your clipboard. This allows you to paste text back into the book using the copied formatting.\n\nNote: This option can be temporarily reversed by holding SHIFT while copying or pasting text."))) .binding(Config.DEFAULT.copyFormattingCodes, () -> config.copyFormattingCodes, (value) -> config.copyFormattingCodes = value) .controller(TickBoxControllerBuilder::create) @@ -71,6 +71,17 @@ public static Screen create(ConfigManager configManager, Screen parent) { .range(8, 128).step(1)) .build()) .build()) + .group(OptionGroup.createBuilder() + .name(Component.literal("Miscellaneous")) + .description(OptionDescription.of(Component.literal("Options that don't fit in the other categories. You will most likely not need to change most of these."))) + .option(Option.createBuilder() + .name(Component.literal("Open vanilla GUI's when holding SHIFT")) + .description(OptionDescription.of(Component.literal("Scribble replaces the vanilla GUI with an exact copy, but sometimes (ex. with other mods) you still want to be able to access the original screen. If this option is enabled, you can access the original screen by holding down SHIFT."))) + .binding(Config.DEFAULT.openVanillaBookScreenOnShift, () -> config.openVanillaBookScreenOnShift, + (value) -> config.openVanillaBookScreenOnShift = value) + .controller(TickBoxControllerBuilder::create) + .build()) + .build()) .build()) .save(() -> { try { diff --git a/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java b/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java index 02e7cc9..b955c2b 100644 --- a/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java +++ b/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java @@ -3,6 +3,7 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.llamalad7.mixinextras.sugar.Local; +import me.chrr.scribble.Scribble; import me.chrr.scribble.screen.ScribbleBookViewScreen; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.Screen; @@ -16,9 +17,8 @@ @Mixin(ClientPacketListener.class) 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()) { - // FIXME: this is temporary, maybe a config option? + public void overrideBookViewScreen(Minecraft instance, Screen screen, Operation original, @Local(name = "bookAccess") BookViewScreen.BookAccess book) { + if (instance.hasShiftDown() && Scribble.config().openVanillaBookScreenOnShift) { 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/LocalPlayerMixin.java b/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java index 87248c0..bcc068a 100644 --- a/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java +++ b/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java @@ -3,6 +3,7 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.llamalad7.mixinextras.sugar.Local; +import me.chrr.scribble.Scribble; import me.chrr.scribble.screen.ScribbleBookEditScreen; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.Screen; @@ -19,8 +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()) { - // FIXME: this is temporary, maybe a config option? + if (instance.hasShiftDown() && Scribble.config().openVanillaBookScreenOnShift) { original.call(instance, screen); } else { // FIXME: ideally, I'd like to avoid even constructing the original BookEditScreen. From 13a4fb0bf6fe9f7386bfc31d71da11dcab7c65e2 Mon Sep 17 00:00:00 2001 From: chrrrs Date: Fri, 26 Dec 2025 14:36:23 +0100 Subject: [PATCH 06/16] fix: make Scribble apply mixins first This way, other mods can override Scribble's behaviour. --- .../java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java | 2 +- src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java b/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java index b955c2b..da5c91d 100644 --- a/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java +++ b/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java @@ -14,7 +14,7 @@ import org.spongepowered.asm.mixin.injection.At; @NullMarked -@Mixin(ClientPacketListener.class) +@Mixin(value = ClientPacketListener.class, priority = 500) 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(name = "bookAccess") BookViewScreen.BookAccess book) { diff --git a/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java b/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java index bcc068a..af905ad 100644 --- a/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java +++ b/src/main/java/me/chrr/scribble/mixin/LocalPlayerMixin.java @@ -16,7 +16,7 @@ import org.spongepowered.asm.mixin.injection.At; @NullMarked -@Mixin(LocalPlayer.class) +@Mixin(value = LocalPlayer.class, priority = 500) 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) { From a830ecdd8a14341155922d6ec9e4c585eaa43abf Mon Sep 17 00:00:00 2001 From: chrrrs Date: Sat, 27 Dec 2025 23:59:20 +0100 Subject: [PATCH 07/16] feat: support double page lecterns --- .../me/chrr/scribble/gui/BookTextWidget.java | 12 +- .../chrr/scribble/gui/PageNumberWidget.java | 11 +- .../chrr/scribble/mixin/MenuScreensMixin.java | 29 ++++ .../scribble/screen/ScribbleBookScreen.java | 20 ++- .../screen/ScribbleBookViewScreen.java | 11 +- .../screen/ScribbleLecternScreen.java | 125 ++++++++++++++++++ src/main/resources/scribble.mixins.json | 3 +- 7 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java create mode 100644 src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java diff --git a/src/main/java/me/chrr/scribble/gui/BookTextWidget.java b/src/main/java/me/chrr/scribble/gui/BookTextWidget.java index 85f7386..34ef5dd 100644 --- a/src/main/java/me/chrr/scribble/gui/BookTextWidget.java +++ b/src/main/java/me/chrr/scribble/gui/BookTextWidget.java @@ -25,6 +25,7 @@ public class BookTextWidget implements TextArea { private boolean visible = true; private boolean hovered = false; + private boolean dimmed = false; private final int x; private final int y; @@ -51,7 +52,10 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) this.hovered = guiGraphics.containsPointInScissor(mouseX, mouseY) && this.areCoordinatesInRectangle(mouseX, mouseY); - this.visitText(guiGraphics.textRenderer(GuiGraphics.HoveredTextEffects.TOOLTIP_AND_CURSOR)); + + ActiveTextCollector textCollector = guiGraphics.textRenderer(GuiGraphics.HoveredTextEffects.TOOLTIP_AND_CURSOR); + textCollector.defaultParameters(textCollector.defaultParameters().withOpacity(this.dimmed ? 0.3f : 1f)); + this.visitText(textCollector); } private void visitText(ActiveTextCollector activeTextCollector) { @@ -134,4 +138,8 @@ public void setFocused(boolean bl) { public boolean isFocused() { return false; } -} + + public void setDimmed(boolean dimmed) { + this.dimmed = dimmed; + } +} \ No newline at end of file diff --git a/src/main/java/me/chrr/scribble/gui/PageNumberWidget.java b/src/main/java/me/chrr/scribble/gui/PageNumberWidget.java index 5abfba1..23d93bb 100644 --- a/src/main/java/me/chrr/scribble/gui/PageNumberWidget.java +++ b/src/main/java/me/chrr/scribble/gui/PageNumberWidget.java @@ -16,6 +16,7 @@ import net.minecraft.network.chat.ComponentUtils; import net.minecraft.network.chat.Style; import net.minecraft.sounds.SoundEvents; +import net.minecraft.util.ARGB; import net.minecraft.util.CommonColors; import net.minecraft.util.Util; import org.jspecify.annotations.NullMarked; @@ -29,6 +30,8 @@ public class PageNumberWidget extends AbstractWidget { private final Consumer onPageChange; private final int anchorX; + private boolean dimmed = false; + private Component text; private Component hoverText; @@ -70,9 +73,11 @@ public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float del graphics.fill(cursorX, this.getY() - 1, cursorX + 1, this.getY() + 1 + font.lineHeight, CommonColors.BLACK); } } else { + int color = ARGB.color(this.dimmed ? 0.3f : 1f, CommonColors.BLACK); + 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(), CommonColors.BLACK, false); + graphics.drawString(this.font, text, this.getX() + this.width - textWidth, this.getY(), color, false); } if (this.isHovered()) { @@ -165,4 +170,8 @@ public void setPageNumber(int page, int total) { this.width = this.font.width(this.text); this.setX(this.anchorX - this.width); } + + public void setDimmed(boolean dimmed) { + this.dimmed = dimmed; + } } diff --git a/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java b/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java new file mode 100644 index 0000000..de4c8e3 --- /dev/null +++ b/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java @@ -0,0 +1,29 @@ +package me.chrr.scribble.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import me.chrr.scribble.Scribble; +import me.chrr.scribble.screen.ScribbleLecternScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.world.inventory.LecternMenu; +import net.minecraft.world.inventory.MenuType; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(MenuScreens.class) +public abstract class MenuScreensMixin { + @WrapOperation(method = "create", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/MenuScreens;getConstructor(Lnet/minecraft/world/inventory/MenuType;)Lnet/minecraft/client/gui/screens/MenuScreens$ScreenConstructor;")) + private static MenuScreens.@Nullable ScreenConstructor overrideLecternScreen(MenuType menuType, Operation> original, @Local(argsOnly = true) Minecraft minecraft) { + if (menuType == MenuType.LECTERN) { + if (!minecraft.hasShiftDown() || !Scribble.config().openVanillaBookScreenOnShift) { + return (MenuScreens.ScreenConstructor) + (menu, inventory, title) -> new ScribbleLecternScreen(menu); + } + } + + return original.call(menuType); + } +} diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java index c36c0d5..bade5b0 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java @@ -59,7 +59,7 @@ protected void init() { this.pageNumbers.clear(); for (int i = 0; i < this.pagesToShow; i++) { - PageNumberWidget widget = new PageNumberWidget(page -> showPage(page - 1, false), x + 148 + i * 126, y + 16, this.font); + PageNumberWidget widget = new PageNumberWidget(page -> jumpToPage(page - 1), x + 148 + i * 126, y + 16, this.font); this.pageNumbers.add(addRenderableWidget(widget)); } @@ -127,9 +127,14 @@ public void updateCurrentPages() { } } + public void jumpToPage(int page) { + this.showPage(page, false); + } + public void showPage(int page, boolean insertIfMissing) { // Insert pages so the requested page exists if needed. - int newPage = Math.clamp(page, 0, this.getTotalPages() - 1); + // (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()) { this.insertEmptyPageAt(this.getTotalPages()); @@ -151,7 +156,7 @@ public void showPage(int page, boolean insertIfMissing) { public void goPageForward(boolean toEnd) { if (toEnd) { - showPage(this.getTotalPages(), false); + showPage(this.getTotalPages() - 1, false); } else { showPage(this.currentPage + this.pagesToShow, true); } @@ -181,6 +186,12 @@ public boolean isInGameUi() { return true; } + @Override + public void onClose() { + this.closeRemoteContainer(); + super.onClose(); + } + public int getBackgroundX() { return this.width / 2 - getBackgroundWidth() / 2; } @@ -212,6 +223,9 @@ public int getMenuControlsY() { //endregion //region Abstract methods + protected void closeRemoteContainer() { + } + protected boolean canInsertPages() { return false; } diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java index 3ed4c77..365ce53 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java @@ -21,7 +21,7 @@ @NullMarked public class ScribbleBookViewScreen extends ScribbleBookScreen { - private final BookViewScreen.BookAccess book; + protected BookViewScreen.BookAccess book; public ScribbleBookViewScreen(BookViewScreen.BookAccess book) { super(Component.translatable("book.view.title")); @@ -80,10 +80,11 @@ private void saveBookToFile() { private void handleClickEvent(ClickEvent event) { switch (event) { - case ClickEvent.ChangePage(int page) -> this.showPage(page - 1, false); - // FIXME: closeContainerOnServer for lecterns. - case ClickEvent.RunCommand(String command) -> - clickCommandAction(Objects.requireNonNull(this.minecraft.player), command, null); + case ClickEvent.ChangePage(int page) -> this.jumpToPage(page - 1); + case ClickEvent.RunCommand(String command) -> { + this.closeRemoteContainer(); + clickCommandAction(Objects.requireNonNull(this.minecraft.player), command, null); + } default -> Screen.defaultHandleGameClickEvent(event, this.minecraft, this); } } diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java new file mode 100644 index 0000000..66bbf9a --- /dev/null +++ b/src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java @@ -0,0 +1,125 @@ +package me.chrr.scribble.screen; + +import me.chrr.scribble.gui.BookTextWidget; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.inventory.BookViewScreen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.client.multiplayer.MultiPlayerGameMode; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerListener; +import net.minecraft.world.inventory.LecternMenu; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.NullMarked; + +import java.util.Objects; +import java.util.Optional; + +@NullMarked +public class ScribbleLecternScreen extends ScribbleBookViewScreen implements MenuAccess, ContainerListener { + private final LecternMenu menu; + private int effectivePage = 0; + + public ScribbleLecternScreen(LecternMenu menu) { + super(BookViewScreen.EMPTY_ACCESS); + + this.menu = menu; + this.menu.addSlotListener(this); + } + + @Override + protected void initMenuControls(int y) { + if (this.minecraft.player != null && this.minecraft.player.mayBuild()) { + this.addRenderableWidget(Button.builder(CommonComponents.GUI_DONE, + button -> this.onClose()) + .pos(this.width / 2 - 98 - 2, y).width(98).build()); + this.addRenderableWidget(Button.builder(Component.translatable("lectern.take_book"), + button -> this.sendButtonClick(LecternMenu.BUTTON_TAKE_BOOK)) + .pos(this.width / 2 + 2, y).width(98).build()); + } else { + super.initMenuControls(y); + } + } + + @Override + public LecternMenu getMenu() { + return this.menu; + } + + @Override + public void slotChanged(AbstractContainerMenu containerToSend, int dataSlotIndex, ItemStack stack) { + this.book = Optional.ofNullable(BookViewScreen.BookAccess.fromItem(this.menu.getBook())) + .orElse(BookViewScreen.EMPTY_ACCESS); + + if (this.currentPage >= this.book.getPageCount()) + this.showPage(this.book.getPageCount() - 1, false); + + this.updateCurrentPages(); + } + + @Override + public void dataChanged(AbstractContainerMenu containerMenu, int dataSlotIndex, int value) { + this.effectivePage = this.menu.getPage(); + this.showPage(this.effectivePage, false); + this.updateEffectivePage(); + } + + @Override + public void goPageForward(boolean toEnd) { + if (toEnd) { + this.jumpToPage(this.getTotalPages() - 1); + } else { + this.sendButtonClick(LecternMenu.BUTTON_NEXT_PAGE); + } + } + + @Override + public void goPageBackward(boolean toStart) { + if (toStart) { + this.jumpToPage(0); + } else { + this.sendButtonClick(LecternMenu.BUTTON_PREV_PAGE); + } + } + + @Override + public void jumpToPage(int page) { + this.sendButtonClick(LecternMenu.BUTTON_PAGE_JUMP_RANGE_START + page); + } + + @Override + protected void closeRemoteContainer() { + if (this.minecraft.player != null) { + this.minecraft.player.closeContainer(); + } + } + + @Override + public void updateCurrentPages() { + super.updateCurrentPages(); + this.updateEffectivePage(); + } + + private void updateEffectivePage() { + // Dim the inactive pages. + for (int i = 0; i < this.pagesToShow; i++) { + int page = this.currentPage + i; + boolean dimmed = page != this.effectivePage; + + ((BookTextWidget) this.textAreas.get(i)).setDimmed(dimmed); + this.pageNumbers.get(i).setDimmed(dimmed); + } + + // Override if the back/forward buttons are visible based on the effective page. + if (this.forwardButton != null && this.backButton != null) { + this.backButton.visible = this.effectivePage > 0; + this.forwardButton.visible = this.effectivePage < this.getTotalPages() - 1; + } + } + + private void sendButtonClick(int pageData) { + MultiPlayerGameMode gameMode = Objects.requireNonNull(this.minecraft.gameMode); + gameMode.handleInventoryButtonClick(this.menu.containerId, pageData); + } +} diff --git a/src/main/resources/scribble.mixins.json b/src/main/resources/scribble.mixins.json index f0eb1ee..a8ec44b 100644 --- a/src/main/resources/scribble.mixins.json +++ b/src/main/resources/scribble.mixins.json @@ -7,7 +7,8 @@ "BookSignScreenMixin", "ClientPacketListenerMixin", "KeyboardHandlerMixin", - "LocalPlayerMixin" + "LocalPlayerMixin", + "MenuScreensMixin" ], "injectors": { "defaultRequire": 1 From 6c6abc2885f8dc597ae274754b5267ef9eb58a6f Mon Sep 17 00:00:00 2001 From: chrrrs Date: Sun, 28 Dec 2025 10:50:39 +0100 Subject: [PATCH 08/16] fix: properly show the modifier button hover overlay --- .../config/YACLConfigScreenFactory.java | 2 +- .../gui/button/ModifierButtonWidget.java | 8 ++--- .../screen/ScribbleBookEditScreen.java | 34 +++++++++---------- .../resources/assets/scribble/lang/en_us.json | 10 +----- 4 files changed, 22 insertions(+), 32 deletions(-) diff --git a/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java b/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java index 3f5e3e6..db2d122 100644 --- a/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java +++ b/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java @@ -73,7 +73,7 @@ public static Screen create(ConfigManager configManager, Screen parent) { .build()) .group(OptionGroup.createBuilder() .name(Component.literal("Miscellaneous")) - .description(OptionDescription.of(Component.literal("Options that don't fit in the other categories. You will most likely not need to change most of these."))) + .description(OptionDescription.of(Component.literal("Options that don't fit in the other categories. You most likely will not need to change most of these."))) .option(Option.createBuilder() .name(Component.literal("Open vanilla GUI's when holding SHIFT")) .description(OptionDescription.of(Component.literal("Scribble replaces the vanilla GUI with an exact copy, but sometimes (ex. with other mods) you still want to be able to access the original screen. If this option is enabled, you can access the original screen by holding down SHIFT."))) diff --git a/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java b/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java index db70e7d..4287757 100644 --- a/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java +++ b/src/main/java/me/chrr/scribble/gui/button/ModifierButtonWidget.java @@ -38,13 +38,11 @@ public ModifierButtonWidget(Component tooltip, Consumer onToggle, int x @Override 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 shift the Z. - if (this.isHoveredOrFocused()) { - // FIXME: shift Z +1. - } + // 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; int u = this.u + (this.isHoveredOrFocused() ? 22 : 0) + (this.toggled ? 44 : 0); - graphics.blit(RenderPipelines.GUI_TEXTURED, WIDGETS_TEXTURE, getX(), getY(), u, v, width, height + 1, 128, 128); + graphics.blit(RenderPipelines.GUI_TEXTURED, WIDGETS_TEXTURE, getX(), getY(), u, v, width, height + offset, 128, 128); } @Override diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java index 7daf8b9..6238702 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java @@ -334,27 +334,27 @@ protected void init() { int x = this.width / 2 + this.getBackgroundWidth() / 2 - 20; int y = this.getBackgroundY() + 12; - // Modifier buttons - boldButton = addRenderableWidget(new ModifierButtonWidget( - Component.translatable("text.scribble.modifier.bold"), - (toggled) -> this.applyFormat(ChatFormatting.BOLD, toggled), - x, y, 0, 0, 22, 19)); - italicButton = addRenderableWidget(new ModifierButtonWidget( - Component.translatable("text.scribble.modifier.italic"), - (toggled) -> this.applyFormat(ChatFormatting.ITALIC, toggled), - x, y + 19, 0, 19, 22, 17)); - underlineButton = addRenderableWidget(new ModifierButtonWidget( - Component.translatable("text.scribble.modifier.underline"), - (toggled) -> this.applyFormat(ChatFormatting.UNDERLINE, toggled), - x, y + 36, 0, 36, 22, 17)); - strikethroughButton = addRenderableWidget(new ModifierButtonWidget( - Component.translatable("text.scribble.modifier.strikethrough"), - (toggled) -> this.applyFormat(ChatFormatting.STRIKETHROUGH, toggled), - x, y + 53, 0, 53, 22, 17)); + // Modifier buttons (but in reverse!) obfuscatedButton = addRenderableWidget(new ModifierButtonWidget( Component.translatable("text.scribble.modifier.obfuscated"), (toggled) -> this.applyFormat(ChatFormatting.OBFUSCATED, toggled), x, y + 70, 0, 70, 22, 18)); + strikethroughButton = addRenderableWidget(new ModifierButtonWidget( + Component.translatable("text.scribble.modifier.strikethrough"), + (toggled) -> this.applyFormat(ChatFormatting.STRIKETHROUGH, toggled), + x, y + 53, 0, 53, 22, 17)); + underlineButton = addRenderableWidget(new ModifierButtonWidget( + Component.translatable("text.scribble.modifier.underline"), + (toggled) -> this.applyFormat(ChatFormatting.UNDERLINE, toggled), + x, y + 36, 0, 36, 22, 17)); + italicButton = addRenderableWidget(new ModifierButtonWidget( + Component.translatable("text.scribble.modifier.italic"), + (toggled) -> this.applyFormat(ChatFormatting.ITALIC, toggled), + x, y + 19, 0, 19, 22, 17)); + boldButton = addRenderableWidget(new ModifierButtonWidget( + Component.translatable("text.scribble.modifier.bold"), + (toggled) -> this.applyFormat(ChatFormatting.BOLD, toggled), + x, y, 0, 0, 22, 19)); // Color swatches colorSwatches = new ArrayList<>(COLORS.length); diff --git a/src/main/resources/assets/scribble/lang/en_us.json b/src/main/resources/assets/scribble/lang/en_us.json index b3f2298..3cbec44 100644 --- a/src/main/resources/assets/scribble/lang/en_us.json +++ b/src/main/resources/assets/scribble/lang/en_us.json @@ -1,15 +1,7 @@ { "config.scribble.title": "Scribble Settings", - "config.scribble.option.copy_formatting_codes": "Copy formatting codes", - "config.scribble.description.copy_formatting_codes": "This option can be temporarily disabled by\npressing SHIFT while copying or pasting text.", - "config.scribble.option.center_book_gui": "Vertically center book GUI's", - "config.scribble.option.show_action_buttons": "Show action buttons", - "config.scribble.option.show_action_buttons.always": "Always", - "config.scribble.option.show_action_buttons.when_editing": "When editing", - "config.scribble.option.show_action_buttons.never": "Never", - "config.scribble.option.edit_history_size": "Undo/redo history size", "text.scribble.action.delete_page": "Delete this page", - "text.scribble.action.insert_new_page": "Insert new page\nbefore 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", "text.scribble.action.undo": "Undo", "text.scribble.action.redo": "Redo", From 80e2b8a60ce0721dbe75b0889c6658bacc13df8a Mon Sep 17 00:00:00 2001 From: chrrrs Date: Sun, 28 Dec 2025 15:00:19 +0100 Subject: [PATCH 09/16] fix: don't name locals in mixins --- .../java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java b/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java index da5c91d..298dc6f 100644 --- a/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java +++ b/src/main/java/me/chrr/scribble/mixin/ClientPacketListenerMixin.java @@ -17,7 +17,7 @@ @Mixin(value = ClientPacketListener.class, priority = 500) 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(name = "bookAccess") BookViewScreen.BookAccess book) { + public void overrideBookViewScreen(Minecraft instance, Screen screen, Operation original, @Local BookViewScreen.BookAccess book) { if (instance.hasShiftDown() && Scribble.config().openVanillaBookScreenOnShift) { original.call(instance, screen); } else { From aa7faa5ee322163f19320da9705510cfec7c5ba3 Mon Sep 17 00:00:00 2001 From: chrrrs Date: Sun, 28 Dec 2025 16:34:38 +0100 Subject: [PATCH 10/16] chore: update Gradle dependencies --- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 2 +- stonecutter.gradle.kts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 20274a3..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-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index b810981..b09220b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,7 +9,7 @@ pluginManagement { } plugins { - id("dev.kikugie.stonecutter") version "0.7-beta.4" + id("dev.kikugie.stonecutter") version "0.8.1" } stonecutter { diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts index 4afe7ad..667e113 100644 --- a/stonecutter.gradle.kts +++ b/stonecutter.gradle.kts @@ -3,7 +3,7 @@ plugins { id("dev.architectury.loom") version "1.13-SNAPSHOT" apply false id("architectury-plugin") version "3.4-SNAPSHOT" apply false id("com.github.johnrengelman.shadow") version "8.1.1" apply false - id("me.modmuss50.mod-publish-plugin") version "0.5.1" apply false + id("me.modmuss50.mod-publish-plugin") version "1.1.0" apply false } stonecutter active "1.21.11" /* [SC] DO NOT EDIT */ From 0943621c6dca5dded87c50f1d6e5ce676a172b19 Mon Sep 17 00:00:00 2001 From: chrrrs Date: Sun, 28 Dec 2025 16:39:26 +0100 Subject: [PATCH 11/16] chore: remove Cloth Config screen --- build.gradle.kts | 1 - fabric/build.gradle.kts | 4 +- gradle.properties | 1 - neoforge/build.gradle.kts | 4 +- src/main/java/me/chrr/scribble/Scribble.java | 1 - .../config/ClothConfigScreenFactory.java | 81 ------------------- 6 files changed, 4 insertions(+), 88 deletions(-) delete mode 100644 src/main/java/me/chrr/scribble/config/ClothConfigScreenFactory.java diff --git a/build.gradle.kts b/build.gradle.kts index bd78c72..5f922bc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -37,7 +37,6 @@ dependencies { modImplementation("net.fabricmc:fabric-loader:${prop("fabric", "loaderVersion")}") - modCompileOnly("me.shedaniel.cloth:cloth-config-fabric:${prop("clothconfig", "version")}") modCompileOnly("dev.isxander:yet-another-config-lib:${prop("yacl", "version")}") } diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index 4b5b46d..7e25cdd 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -130,13 +130,13 @@ publishMods { projectId.set(prop("modrinth", "id")) accessToken.set(providers.environmentVariable("MODRINTH_TOKEN")) minecraftVersions.addAll(versions) - optional("cloth-config") + optional("yacl") } curseforge { projectId.set(prop("curseforge", "id")) accessToken.set(providers.environmentVariable("CURSEFORGE_TOKEN")) minecraftVersions.addAll(versions) - optional("cloth-config") + optional("yacl") } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index e1489db..1a62523 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,7 +22,6 @@ neoforge.version=[versioned] # Dependencies modmenu.version=11.0.2 -clothconfig.version=15.0.140 yacl.version=3.8.1+1.21.11-fabric # Distribution diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts index 45d07c8..2495c23 100644 --- a/neoforge/build.gradle.kts +++ b/neoforge/build.gradle.kts @@ -123,13 +123,13 @@ publishMods { projectId.set(prop("modrinth", "id")) accessToken.set(providers.environmentVariable("MODRINTH_TOKEN")) minecraftVersions.addAll(versions) - optional("cloth-config") + optional("yacl") } curseforge { projectId.set(prop("curseforge", "id")) accessToken.set(providers.environmentVariable("CURSEFORGE_TOKEN")) minecraftVersions.addAll(versions) - optional("cloth-config") + optional("yacl") } } \ No newline at end of file diff --git a/src/main/java/me/chrr/scribble/Scribble.java b/src/main/java/me/chrr/scribble/Scribble.java index 654221a..5cb04a6 100644 --- a/src/main/java/me/chrr/scribble/Scribble.java +++ b/src/main/java/me/chrr/scribble/Scribble.java @@ -60,7 +60,6 @@ public abstract static class Platform { public final Path BOOK_DIR = getGameDir().resolve("books"); public final String VERSION = getModVersion(); - public final boolean HAS_CLOTH_CONFIG = isModLoaded("cloth_config") || isModLoaded("cloth-config"); public final boolean HAS_YACL = isModLoaded("yet_another_config_lib_v3"); protected abstract boolean isModLoaded(String modId); diff --git a/src/main/java/me/chrr/scribble/config/ClothConfigScreenFactory.java b/src/main/java/me/chrr/scribble/config/ClothConfigScreenFactory.java deleted file mode 100644 index 6fef0c9..0000000 --- a/src/main/java/me/chrr/scribble/config/ClothConfigScreenFactory.java +++ /dev/null @@ -1,81 +0,0 @@ -package me.chrr.scribble.config; - -import me.chrr.scribble.Scribble; -import me.shedaniel.clothconfig2.api.ConfigBuilder; -import me.shedaniel.clothconfig2.api.ConfigCategory; -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; -import org.jspecify.annotations.NullMarked; - -import java.io.IOException; - -@NullMarked -public class ClothConfigScreenFactory { - private ClothConfigScreenFactory() { - } - - public static Screen create(ConfigManager configManager, Screen parent) { - ConfigBuilder builder = ConfigBuilder.create() - .setParentScreen(parent) - .setTitle(Component.translatable("config.scribble.title")); - - builder.setSavingRunnable(() -> { - try { - configManager.save(); - } catch (IOException e) { - Scribble.LOGGER.error("could not save config", e); - } - }); - - Config config = configManager.getConfig(); - - ConfigCategory category = builder.getOrCreateCategory(Component.empty()); - ConfigEntryBuilder entryBuilder = builder.entryBuilder(); - - category.addEntry(entryBuilder.startBooleanToggle( - Component.translatable("config.scribble.option.copy_formatting_codes"), - config.copyFormattingCodes - ) - .setDefaultValue(Config.DEFAULT.copyFormattingCodes) - .setTooltip(Component.translatable("config.scribble.description.copy_formatting_codes")) - .setSaveConsumer((value) -> config.copyFormattingCodes = value) - .build()); - - category.addEntry(entryBuilder.startBooleanToggle( - Component.translatable("config.scribble.option.center_book_gui"), - config.centerBookGui - ) - .setDefaultValue(Config.DEFAULT.centerBookGui) - .setSaveConsumer((value) -> config.centerBookGui = value) - .build()); - - category.addEntry(entryBuilder.startEnumSelector( - Component.translatable("config.scribble.option.show_action_buttons"), - Config.ShowActionButtons.class, config.showActionButtons - ) - .setEnumNameProvider((opt) -> - Component.translatable("config.scribble.option.show_action_buttons." + opt.name().toLowerCase())) - .setDefaultValue(Config.DEFAULT.showActionButtons) - .setSaveConsumer((value) -> config.showActionButtons = value) - .build()); - - category.addEntry(entryBuilder.startIntSlider( - Component.translatable("config.scribble.option.edit_history_size"), - config.editHistorySize, 8, 128 - ) - .setDefaultValue(Config.DEFAULT.editHistorySize) - .setSaveConsumer((value) -> config.editHistorySize = value) - .build()); - - category.addEntry(entryBuilder.startIntField( - Component.literal("Pages to show"), - config.pagesToShow - ) - .setDefaultValue(Config.DEFAULT.pagesToShow) - .setSaveConsumer((value) -> config.pagesToShow = value) - .build()); - - return builder.build(); - } -} From 9b1cbd88374d49c12e945fdd4ff1094588f250fd Mon Sep 17 00:00:00 2001 From: chrrrs Date: Sun, 28 Dec 2025 16:51:30 +0100 Subject: [PATCH 12/16] feat: add a config option to hide the formatting buttons --- .../java/me/chrr/scribble/config/Config.java | 1 + .../config/YACLConfigScreenFactory.java | 7 ++ .../screen/ScribbleBookEditScreen.java | 88 ++++++++++--------- 3 files changed, 53 insertions(+), 43 deletions(-) diff --git a/src/main/java/me/chrr/scribble/config/Config.java b/src/main/java/me/chrr/scribble/config/Config.java index 586ab13..ba220bf 100644 --- a/src/main/java/me/chrr/scribble/config/Config.java +++ b/src/main/java/me/chrr/scribble/config/Config.java @@ -9,6 +9,7 @@ public class Config { public int version = 3; public boolean copyFormattingCodes = true; public boolean centerBookGui = true; + public boolean showFormattingButtons = true; public ShowActionButtons showActionButtons = ShowActionButtons.WHEN_EDITING; public int editHistorySize = 32; public int pagesToShow = 1; diff --git a/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java b/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java index db2d122..70b962a 100644 --- a/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java +++ b/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java @@ -38,6 +38,13 @@ public static Screen create(ConfigManager configManager, Screen parent) { (value) -> config.centerBookGui = value) .controller(TickBoxControllerBuilder::create) .build()) + .option(Option.createBuilder() + .name(Component.literal("Show formatting buttons")) + .description(OptionDescription.of(Component.literal("Formatting buttons are the color/modifier buttons on the right of the page. With this option, you can hide these buttons.\n\nNote: You'll always be able to access their functionality using their hotkeys."))) + .binding(Config.DEFAULT.showFormattingButtons, () -> config.showFormattingButtons, + (value) -> config.showFormattingButtons = value) + .controller(TickBoxControllerBuilder::create) + .build()) .option(Option.createBuilder() .name(Component.literal("Show action buttons")) .description(OptionDescription.of(Component.literal("Action buttons are the white buttons on the left of the page. This option determines when these action buttons should be shown or hidden.\n\nThe 'Show when editing' option shows the action buttons when editing a book using a book & quill, but hides them when reading a written book."))) diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java index 6238702..5e2b297 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java @@ -309,7 +309,8 @@ public boolean keyPressed(KeyEvent keyEvent) { // On Ctrl-Z, undo. if ((KeyboardUtil.isKey(keyEvent.key(), "Z") && !keyEvent.hasShiftDown() && undoButton != null && undoButton.active)) { - undoButton.onPress(keyEvent); + this.commandManager.tryUndo(); + this.invalidateActionButtons(); return true; } @@ -317,7 +318,8 @@ public boolean keyPressed(KeyEvent keyEvent) { if (((KeyboardUtil.isKey(keyEvent.key(), "Z") && keyEvent.hasShiftDown()) || (KeyboardUtil.isKey(keyEvent.key(), "Y") && !keyEvent.hasShiftDown())) && redoButton != null && redoButton.active) { - redoButton.onPress(keyEvent); + this.commandManager.tryRedo(); + this.invalidateActionButtons(); return true; } } @@ -331,46 +333,48 @@ public boolean keyPressed(KeyEvent keyEvent) { protected void init() { super.init(); - int x = this.width / 2 + this.getBackgroundWidth() / 2 - 20; - int y = this.getBackgroundY() + 12; - - // Modifier buttons (but in reverse!) - obfuscatedButton = addRenderableWidget(new ModifierButtonWidget( - Component.translatable("text.scribble.modifier.obfuscated"), - (toggled) -> this.applyFormat(ChatFormatting.OBFUSCATED, toggled), - x, y + 70, 0, 70, 22, 18)); - strikethroughButton = addRenderableWidget(new ModifierButtonWidget( - Component.translatable("text.scribble.modifier.strikethrough"), - (toggled) -> this.applyFormat(ChatFormatting.STRIKETHROUGH, toggled), - x, y + 53, 0, 53, 22, 17)); - underlineButton = addRenderableWidget(new ModifierButtonWidget( - Component.translatable("text.scribble.modifier.underline"), - (toggled) -> this.applyFormat(ChatFormatting.UNDERLINE, toggled), - x, y + 36, 0, 36, 22, 17)); - italicButton = addRenderableWidget(new ModifierButtonWidget( - Component.translatable("text.scribble.modifier.italic"), - (toggled) -> this.applyFormat(ChatFormatting.ITALIC, toggled), - x, y + 19, 0, 19, 22, 17)); - boldButton = addRenderableWidget(new ModifierButtonWidget( - Component.translatable("text.scribble.modifier.bold"), - (toggled) -> this.applyFormat(ChatFormatting.BOLD, toggled), - x, y, 0, 0, 22, 19)); - - // Color swatches - colorSwatches = new ArrayList<>(COLORS.length); - for (int i = 0; i < COLORS.length; i++) { - int dx = (i % 2) * 8; - int dy = (i / 2) * 8; - - ChatFormatting color = COLORS[i]; - colorSwatches.add(addRenderableWidget(new ColorSwatchWidget( - Component.translatable("text.scribble.color." + color.getName()), color, - () -> this.applyFormat(color, true), - x + 3 + dx, y + 95 + dy, 8, 8 - ))); - } + if (Scribble.config().showFormattingButtons) { + int x = this.width / 2 + this.getBackgroundWidth() / 2 - 20; + int y = this.getBackgroundY() + 12; + + // Modifier buttons (but in reverse!) + obfuscatedButton = addRenderableWidget(new ModifierButtonWidget( + Component.translatable("text.scribble.modifier.obfuscated"), + (toggled) -> this.applyFormat(ChatFormatting.OBFUSCATED, toggled), + x, y + 70, 0, 70, 22, 18)); + strikethroughButton = addRenderableWidget(new ModifierButtonWidget( + Component.translatable("text.scribble.modifier.strikethrough"), + (toggled) -> this.applyFormat(ChatFormatting.STRIKETHROUGH, toggled), + x, y + 53, 0, 53, 22, 17)); + underlineButton = addRenderableWidget(new ModifierButtonWidget( + Component.translatable("text.scribble.modifier.underline"), + (toggled) -> this.applyFormat(ChatFormatting.UNDERLINE, toggled), + x, y + 36, 0, 36, 22, 17)); + italicButton = addRenderableWidget(new ModifierButtonWidget( + Component.translatable("text.scribble.modifier.italic"), + (toggled) -> this.applyFormat(ChatFormatting.ITALIC, toggled), + x, y + 19, 0, 19, 22, 17)); + boldButton = addRenderableWidget(new ModifierButtonWidget( + Component.translatable("text.scribble.modifier.bold"), + (toggled) -> this.applyFormat(ChatFormatting.BOLD, toggled), + x, y, 0, 0, 22, 19)); + + // Color swatches + colorSwatches = new ArrayList<>(COLORS.length); + for (int i = 0; i < COLORS.length; i++) { + int dx = (i % 2) * 8; + int dy = (i / 2) * 8; + + ChatFormatting color = COLORS[i]; + colorSwatches.add(addRenderableWidget(new ColorSwatchWidget( + Component.translatable("text.scribble.color." + color.getName()), color, + () -> this.applyFormat(color, true), + x + 3 + dx, y + 95 + dy, 8, 8 + ))); + } - this.invalidateFormattingButtons(); + this.invalidateFormattingButtons(); + } } private void applyFormat(ChatFormatting formatting, boolean enabled) { @@ -390,8 +394,6 @@ private void invalidateFormattingButtons() { if (editBox == null) return; - // Sometimes, these buttons get invalidated on screen initialize, when the buttons don't exist yet. - // We can just return in that case. if (boldButton == null || italicButton == null || underlineButton == null || strikethroughButton == null || obfuscatedButton == null) return; From d71c0998912dc48c4bc0efbda6116fa93b4a27d5 Mon Sep 17 00:00:00 2001 From: chrrrs Date: Sun, 28 Dec 2025 17:02:08 +0100 Subject: [PATCH 13/16] fix: make sure the lectern mixin works on NeoForge --- .../chrr/scribble/mixin/MenuScreensMixin.java | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java b/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java index de4c8e3..75baf66 100644 --- a/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java +++ b/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java @@ -1,29 +1,35 @@ package me.chrr.scribble.mixin; +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import com.llamalad7.mixinextras.sugar.Local; import me.chrr.scribble.Scribble; import me.chrr.scribble.screen.ScribbleLecternScreen; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; import net.minecraft.world.inventory.LecternMenu; import net.minecraft.world.inventory.MenuType; -import org.jspecify.annotations.Nullable; +import org.jspecify.annotations.NullMarked; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; +@NullMarked @Mixin(MenuScreens.class) public abstract class MenuScreensMixin { - @WrapOperation(method = "create", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/MenuScreens;getConstructor(Lnet/minecraft/world/inventory/MenuType;)Lnet/minecraft/client/gui/screens/MenuScreens$ScreenConstructor;")) - private static MenuScreens.@Nullable ScreenConstructor overrideLecternScreen(MenuType menuType, Operation> original, @Local(argsOnly = true) Minecraft minecraft) { - if (menuType == MenuType.LECTERN) { - if (!minecraft.hasShiftDown() || !Scribble.config().openVanillaBookScreenOnShift) { - return (MenuScreens.ScreenConstructor) - (menu, inventory, title) -> new ScribbleLecternScreen(menu); - } + @WrapMethod(method = "register") + private static > void overrideLecternScreen(MenuType type, MenuScreens.ScreenConstructor factory, Operation original) { + if (type == MenuType.LECTERN) { + original.call(type, (MenuScreens.ScreenConstructor) (menu, inventory, title) -> { + Minecraft minecraft = Minecraft.getInstance(); + if (!minecraft.hasShiftDown() || !Scribble.config().openVanillaBookScreenOnShift) { + //noinspection unchecked: S is technically not the same here, but it doesn't matter. + return (S) new ScribbleLecternScreen(menu); + } else { + return factory.create(menu, inventory, title); + } + }); + } else { + original.call(type, factory); } - - return original.call(menuType); } } From a370c2cdec5de92e818920f439b77745441e5eeb Mon Sep 17 00:00:00 2001 From: chrrrs Date: Sun, 28 Dec 2025 17:04:28 +0100 Subject: [PATCH 14/16] fix: make lecterns not pause the game --- .../java/me/chrr/scribble/screen/ScribbleLecternScreen.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java b/src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java index 66bbf9a..a0db774 100644 --- a/src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java +++ b/src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java @@ -95,6 +95,11 @@ protected void closeRemoteContainer() { } } + @Override + public boolean isPauseScreen() { + return false; + } + @Override public void updateCurrentPages() { super.updateCurrentPages(); From 60cebeb642e8e529ce2f072002488e96d347f104 Mon Sep 17 00:00:00 2001 From: chrrrs Date: Sun, 28 Dec 2025 17:44:31 +0100 Subject: [PATCH 15/16] fix: make sure the mod actually builds --- build.gradle.kts | 2 +- fabric/build.gradle.kts | 8 +++----- gradle.properties | 3 --- neoforge/build.gradle.kts | 10 ++++------ .../1.21.7.accesswidener => scribble.accesswidener} | 0 stonecutter.gradle.kts | 2 +- versions/1.21.11/gradle.properties | 2 -- 7 files changed, 9 insertions(+), 18 deletions(-) rename src/main/resources/{aw/1.21.7.accesswidener => scribble.accesswidener} (100%) diff --git a/build.gradle.kts b/build.gradle.kts index 5f922bc..5ac27c6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,7 +41,7 @@ dependencies { } loom { - accessWidenerPath = rootProject.file("src/main/resources/aw/${prop("mod", "accesswidener")}.accesswidener") + accessWidenerPath = rootProject.file("src/main/resources/scribble.accesswidener") } java { diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index 7e25cdd..33470bc 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -3,7 +3,7 @@ import me.modmuss50.mpp.ReleaseType plugins { id("dev.architectury.loom") id("architectury-plugin") - id("com.github.johnrengelman.shadow") + id("com.gradleup.shadow") id("me.modmuss50.mod-publish-plugin") } @@ -63,10 +63,8 @@ loom { } java { - val java = if (stonecutter.eval(current, ">=1.20.5")) - JavaVersion.VERSION_21 else JavaVersion.VERSION_17 - targetCompatibility = java - sourceCompatibility = java + targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_21 } tasks { diff --git a/gradle.properties b/gradle.properties index 1a62523..9c29bb4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,9 +8,6 @@ mod.name=Scribble mod.version=1.6.7 mod.group=me.chrr -# Name of the accesswidener file used -mod.accesswidener=[versioned] - # Fabric # check these on https://modmuss50.me/fabric.html fabric.loaderVersion=[versioned] diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts index 2495c23..e3e85a2 100644 --- a/neoforge/build.gradle.kts +++ b/neoforge/build.gradle.kts @@ -3,7 +3,7 @@ import me.modmuss50.mpp.ReleaseType plugins { id("dev.architectury.loom") id("architectury-plugin") - id("com.github.johnrengelman.shadow") + id("com.gradleup.shadow") id("me.modmuss50.mod-publish-plugin") } @@ -56,10 +56,8 @@ loom { } java { - val java = if (stonecutter.eval(minecraft, ">=1.20.5")) - JavaVersion.VERSION_21 else JavaVersion.VERSION_17 - targetCompatibility = java - sourceCompatibility = java + targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_21 } tasks { @@ -69,7 +67,7 @@ tasks { } remapJar { - atAccessWideners.add("aw/${common.prop("mod", "accesswidener")}.accesswidener") + atAccessWideners.add("scribble.accesswidener") inputFile.set(shadowJar.get().archiveFile) archiveClassifier = null dependsOn(shadowJar) diff --git a/src/main/resources/aw/1.21.7.accesswidener b/src/main/resources/scribble.accesswidener similarity index 100% rename from src/main/resources/aw/1.21.7.accesswidener rename to src/main/resources/scribble.accesswidener diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts index 667e113..85648c1 100644 --- a/stonecutter.gradle.kts +++ b/stonecutter.gradle.kts @@ -2,7 +2,7 @@ plugins { id("dev.kikugie.stonecutter") id("dev.architectury.loom") version "1.13-SNAPSHOT" apply false id("architectury-plugin") version "3.4-SNAPSHOT" apply false - id("com.github.johnrengelman.shadow") version "8.1.1" apply false + id("com.gradleup.shadow") version "9.3.0" apply false id("me.modmuss50.mod-publish-plugin") version "1.1.0" apply false } diff --git a/versions/1.21.11/gradle.properties b/versions/1.21.11/gradle.properties index 74fb99d..e205ebd 100644 --- a/versions/1.21.11/gradle.properties +++ b/versions/1.21.11/gradle.properties @@ -2,8 +2,6 @@ platform.versions=1.21.11 minecraft.version=1.21.11 parchment.version=1.21.11:2025.12.20 -mod.accesswidener=1.21.7 - fabric.loaderVersion=0.18.2 fabric.apiVersion=0.139.4+1.21.11 From 54e8cadaa4eff4e9ddd230a1b988a926c97992d2 Mon Sep 17 00:00:00 2001 From: chrrrs Date: Sun, 28 Dec 2025 19:10:47 +0100 Subject: [PATCH 16/16] chore: update to fabric resource loader v1 --- fabric/build.gradle.kts | 3 ++- fabric/src/main/resources/fabric.mod.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index 33470bc..29ac286 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -44,7 +44,8 @@ dependencies { fun fabricApiModule(name: String) = modImplementation(fabricApi.module(name, common.prop("fabric", "apiVersion"))) - include(fabricApiModule("fabric-resource-loader-v0")!!) + include(fabricApiModule("fabric-api-base")!!) + include(fabricApiModule("fabric-resource-loader-v1")!!) modCompileOnly("com.terraformersmc:modmenu:${common.prop("modmenu", "version")}") diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index af3d3df..418ed9a 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -32,7 +32,7 @@ "depends": { "minecraft": "${minecraft}", "fabricloader": ">=0.15", - "fabric-resource-loader-v0": "*" + "fabric-resource-loader-v1": "*" }, "breaks": { "fixbookgui": "*"