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..5ac27c6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,18 +22,26 @@ 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")}") + modCompileOnly("dev.isxander:yet-another-config-lib:${prop("yacl", "version")}") } 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 2d91b41..29ac286 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") } @@ -44,10 +44,10 @@ 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")}") - 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 } @@ -64,10 +64,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 { @@ -131,13 +129,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/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..9f6fb99 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,17 @@ import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; -import me.chrr.scribble.config.ClothConfigScreenFactory; +import me.chrr.scribble.Scribble; import net.fabricmc.loader.api.FabricLoader; +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 (FabricLoader.getInstance().isModLoaded("yet_another_config_lib_v3")) { + 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..418ed9a 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -27,15 +27,12 @@ ], "modmenu": [ "me.chrr.scribble.fabric.ModMenuCompat" - ], - "debugify": [ - "me.chrr.scribble.fabric.DebugifyCompat" ] }, "depends": { "minecraft": "${minecraft}", "fabricloader": ">=0.15", - "fabric-resource-loader-v0": "*" + "fabric-resource-loader-v1": "*" }, "breaks": { "fixbookgui": "*" diff --git a/gradle.properties b/gradle.properties index 4691d4d..9c29bb4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,15 +1,13 @@ org.gradle.jvmargs=-Xmx1G platform.versions=[versioned] +parchment.version=[versioned] # Mod details 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] @@ -21,7 +19,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/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/neoforge/build.gradle.kts b/neoforge/build.gradle.kts index 45d07c8..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) @@ -123,13 +121,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/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/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/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..5cb04a6 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,35 @@ 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_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 deleted file mode 100644 index fbc5131..0000000 --- a/src/main/java/me/chrr/scribble/config/ClothConfigScreenFactory.java +++ /dev/null @@ -1,71 +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 java.io.IOException; - -public class ClothConfigScreenFactory { - private ClothConfigScreenFactory() { - } - - public static Screen create(Screen parent) { - ConfigBuilder builder = ConfigBuilder.create() - .setParentScreen(parent) - .setTitle(Component.translatable("config.scribble.title")); - - builder.setSavingRunnable(() -> { - try { - Scribble.CONFIG_MANAGER.save(); - } catch (IOException e) { - Scribble.LOGGER.error("could not save config", e); - } - }); - - Config config = Scribble.CONFIG_MANAGER.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()); - - 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..ba220bf 100644 --- a/src/main/java/me/chrr/scribble/config/Config.java +++ b/src/main/java/me/chrr/scribble/config/Config.java @@ -1,13 +1,19 @@ package me.chrr.scribble.config; +import org.jspecify.annotations.NullMarked; + +@NullMarked public class Config { public static final Config DEFAULT = new 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; + public boolean openVanillaBookScreenOnShift = false; @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..70b962a --- /dev/null +++ b/src/main/java/me/chrr/scribble/config/YACLConfigScreenFactory.java @@ -0,0 +1,103 @@ +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 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."))) + .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 reversed 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()) + .group(OptionGroup.createBuilder() + .name(Component.literal("Miscellaneous")) + .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."))) + .binding(Config.DEFAULT.openVanillaBookScreenOnShift, () -> config.openVanillaBookScreenOnShift, + (value) -> config.openVanillaBookScreenOnShift = value) + .controller(TickBoxControllerBuilder::create) + .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..34ef5dd --- /dev/null +++ b/src/main/java/me/chrr/scribble/gui/BookTextWidget.java @@ -0,0 +1,145 @@ +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 boolean dimmed = 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); + + 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) { + 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; + } + + 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 0aef564..23d93bb 100644 --- a/src/main/java/me/chrr/scribble/gui/PageNumberWidget.java +++ b/src/main/java/me/chrr/scribble/gui/PageNumberWidget.java @@ -16,17 +16,22 @@ 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; import org.lwjgl.glfw.GLFW; import java.util.function.Consumer; +@NullMarked public class PageNumberWidget extends AbstractWidget { private final Font font; private final Consumer onPageChange; private final int anchorX; + private boolean dimmed = false; + private Component text; private Component hoverText; @@ -68,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()) { @@ -163,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/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..4287757 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,34 +17,32 @@ * 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; } @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/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..7942fce 100644 --- a/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java +++ b/src/main/java/me/chrr/scribble/mixin/BookSignScreenMixin.java @@ -3,6 +3,9 @@ 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; @@ -10,28 +13,50 @@ 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.injection.*; +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 extends Screen { - // Dummy constructor to match super class. - private BookSignScreenMixin() { - super(null); +public abstract class BookSignScreenMixin extends Screen implements SetReturnScreen { + private BookSignScreenMixin(Component title) { + super(title); } + //region Return Screen + @Unique + public @Nullable Screen scribble$returnScreen = null; + + @Override + public void scribble$setReturnScreen(Screen screen) { + this.scribble$returnScreen = screen; + } + + @WrapOperation(method = "method_71541", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;setScreen(Lnet/minecraft/client/gui/screens/Screen;)V")) + 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.getBookScreenYOffset(height) + 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.getBookScreenYOffset(height)); + widget.setY(widget.getY() + scribble$getYOffset()); } return original.call(instance, guiEventListener); @@ -42,7 +67,7 @@ public T shiftButton @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)); + graphics.pose().translate(0f, scribble$getYOffset()); } // At the end of rendering, we need to pop those matrices we pushed. @@ -50,4 +75,15 @@ public void translateRender(GuiGraphics graphics, int mouseX, int mouseY, float 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/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..298dc6f --- /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.Scribble; +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(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 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. + 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..af905ad --- /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.Scribble; +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(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) { + if (instance.hasShiftDown() && Scribble.config().openVanillaBookScreenOnShift) { + 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/mixin/MenuScreensMixin.java b/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java new file mode 100644 index 0000000..75baf66 --- /dev/null +++ b/src/main/java/me/chrr/scribble/mixin/MenuScreensMixin.java @@ -0,0 +1,35 @@ +package me.chrr.scribble.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +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.NullMarked; +import org.spongepowered.asm.mixin.Mixin; + +@NullMarked +@Mixin(MenuScreens.class) +public abstract class MenuScreensMixin { + @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); + } + } +} 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..5e2b297 --- /dev/null +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookEditScreen.java @@ -0,0 +1,511 @@ +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 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; +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 final List insertPageButtons = new ArrayList<>(); + + 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 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) -> { + @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)) { + this.commandManager.tryUndo(); + this.invalidateActionButtons(); + 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) { + this.commandManager.tryRedo(); + this.invalidateActionButtons(); + return true; + } + } + + return super.keyPressed(keyEvent); + } + //endregion + + //region Formatting + @Override + protected void init() { + super.init(); + + 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(); + } + } + + 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; + + 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.showPage(page, false); + 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..bade5b0 --- /dev/null +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookScreen.java @@ -0,0 +1,251 @@ +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 -> jumpToPage(page - 1), 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)); + initPageButtons(y + 157); + 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 jumpToPage(int page) { + this.showPage(page, false); + } + + public void showPage(int page, boolean insertIfMissing) { + // Insert pages so the requested page exists if needed. + // (note that min+max instead of clamp here is deliberate) + int newPage = Math.max(Math.min(page, this.getTotalPages() - 1), 0); + 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() - 1, 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; + } + + @Override + public void onClose() { + this.closeRemoteContainer(); + super.onClose(); + } + + 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 2 + this.height / 3 - getMenuHeight() / 3; + } else { + return 2; + } + } + + public int getBackgroundWidth() { + return this.pagesToShow * 126 + 66; + } + + public int getBackgroundHeight() { + return 182; + } + + public static int getMenuHeight() { + return 194 + 20; + } + + public int getMenuControlsY() { + return this.getBackgroundY() + getMenuHeight() - 20; + } + //endregion + + //region Abstract methods + protected void closeRemoteContainer() { + } + + protected boolean canInsertPages() { + return false; + } + + protected void insertEmptyPageAt(int page) { + } + + protected abstract boolean shouldShowActionButtons(); + + 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); + + 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..365ce53 --- /dev/null +++ b/src/main/java/me/chrr/scribble/screen/ScribbleBookViewScreen.java @@ -0,0 +1,91 @@ +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 { + protected 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.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..a0db774 --- /dev/null +++ b/src/main/java/me/chrr/scribble/screen/ScribbleLecternScreen.java @@ -0,0 +1,130 @@ +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 boolean isPauseScreen() { + return false; + } + + @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/assets/scribble/lang/en_us.json b/src/main/resources/assets/scribble/lang/en_us.json index 6f98ccf..3cbec44 100644 --- a/src/main/resources/assets/scribble/lang/en_us.json +++ b/src/main/resources/assets/scribble/lang/en_us.json @@ -1,21 +1,15 @@ { - "config.scribble.title": "Scribble Config", - "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 page", - "text.scribble.action.insert_new_page": "Insert new page\nbefore current", + "config.scribble.title": "Scribble Settings", + "text.scribble.action.delete_page": "Delete this page", + "text.scribble.action.insert_new_page": "Insert new page\nbefore this one", + "text.scribble.action.insert_new_page_here": "Insert new page here", "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", 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 0000000..662f804 Binary files /dev/null and b/src/main/resources/assets/scribble/textures/gui/book_2.png differ 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 80ab97a..16a81a7 100644 Binary files a/src/main/resources/assets/scribble/textures/gui/scribble_widgets.png and b/src/main/resources/assets/scribble/textures/gui/scribble_widgets.png differ 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/src/main/resources/scribble.mixins.json b/src/main/resources/scribble.mixins.json index 2339ffd..a8ec44b 100644 --- a/src/main/resources/scribble.mixins.json +++ b/src/main/resources/scribble.mixins.json @@ -4,11 +4,11 @@ "package": "me.chrr.scribble.mixin", "compatibilityLevel": "JAVA_17", "client": [ - "BookEditScreenMixin", - "BookViewScreenMixin", "BookSignScreenMixin", + "ClientPacketListenerMixin", "KeyboardHandlerMixin", - "LecternScreenMixin" + "LocalPlayerMixin", + "MenuScreensMixin" ], "injectors": { "defaultRequire": 1 diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts index 4afe7ad..85648c1 100644 --- a/stonecutter.gradle.kts +++ b/stonecutter.gradle.kts @@ -2,8 +2,8 @@ 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("me.modmuss50.mod-publish-plugin") version "0.5.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 } stonecutter active "1.21.11" /* [SC] DO NOT EDIT */ diff --git a/versions/1.21.11/gradle.properties b/versions/1.21.11/gradle.properties index c7b9e14..e205ebd 100644 --- a/versions/1.21.11/gradle.properties +++ b/versions/1.21.11/gradle.properties @@ -1,7 +1,6 @@ platform.versions=1.21.11 minecraft.version=1.21.11 - -mod.accesswidener=1.21.7 +parchment.version=1.21.11:2025.12.20 fabric.loaderVersion=0.18.2 fabric.apiVersion=0.139.4+1.21.11