From c0c9bce372e5e450246565ad2de26422a17f4c29 Mon Sep 17 00:00:00 2001 From: Frederik van der Els Date: Tue, 7 Jul 2026 21:07:03 +0200 Subject: [PATCH 1/3] Allow copying chat lines when translating using /ctranslate --- .../command/TranslateCommand.java | 4 +- .../interfaces/IChatScreen.java | 5 ++ .../translate/ChatComponent$1Mixin.java | 57 +++++++++++++++++++ .../commands/translate/ChatScreenMixin.java | 34 +++++++++++ .../commands/translate/package-info.java | 4 ++ src/main/resources/clientcommands.aw | 6 ++ src/main/resources/mixins.clientcommands.json | 2 + 7 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/earthcomputer/clientcommands/interfaces/IChatScreen.java create mode 100644 src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatComponent$1Mixin.java create mode 100644 src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatScreenMixin.java create mode 100644 src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/package-info.java diff --git a/src/main/java/net/earthcomputer/clientcommands/command/TranslateCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/TranslateCommand.java index c345e4da6..faa0f6ccc 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/TranslateCommand.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/TranslateCommand.java @@ -25,6 +25,8 @@ public class TranslateCommand { + public static final String LITERAL = "ctranslate"; + private static final SimpleCommandExceptionType UNKNOWN_ERROR_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.ctranslate.unknownError")); private static final String URL_FORMAT = "https://translate.googleapis.com/translate_a/single?client=gtx&dt=t&sl=%s&tl=%s&q=%s"; @@ -34,7 +36,7 @@ public class TranslateCommand { private static final Duration DURATION = Duration.ofSeconds(5); public static void register(CommandDispatcher dispatcher) { - dispatcher.register(literal("ctranslate") + dispatcher.register(literal(LITERAL) .then(argument("query", translationQuery()) .executes(ctx -> translate(ctx.getSource(), getTranslationQuery(ctx, "query"))))); } diff --git a/src/main/java/net/earthcomputer/clientcommands/interfaces/IChatScreen.java b/src/main/java/net/earthcomputer/clientcommands/interfaces/IChatScreen.java new file mode 100644 index 000000000..bc64735cf --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/interfaces/IChatScreen.java @@ -0,0 +1,5 @@ +package net.earthcomputer.clientcommands.interfaces; + +public interface IChatScreen { + boolean clientcommands_isTranslating(); +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatComponent$1Mixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatComponent$1Mixin.java new file mode 100644 index 000000000..061538162 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatComponent$1Mixin.java @@ -0,0 +1,57 @@ +package net.earthcomputer.clientcommands.mixin.commands.translate; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.earthcomputer.clientcommands.interfaces.IChatScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ActiveTextCollector; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.TextAlignment; +import net.minecraft.client.gui.components.ChatComponent; +import net.minecraft.client.gui.screens.ChatScreen; +import net.minecraft.client.multiplayer.chat.GuiMessage; +import net.minecraft.client.renderer.state.gui.GuiTextRenderState; +import net.minecraft.util.ARGB; +import net.minecraft.util.FormattedCharSequence; +import org.spongepowered.asm.mixin.Debug; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Debug(export = true) +@Mixin(targets = "net/minecraft/client/gui/components/ChatComponent$1") +public abstract class ChatComponent$1Mixin { + @WrapOperation(method = "accept", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/components/ChatComponent$ChatGraphicsAccess;handleMessage(IFLnet/minecraft/util/FormattedCharSequence;)Z")) + private boolean copyText(ChatComponent.ChatGraphicsAccess instance, int textTop, float opacity, FormattedCharSequence text, Operation original, GuiMessage.Line line) { + if (shouldCallOriginal(instance, textTop, text, line)) { + return original.call(instance, textTop, opacity, text); + } + // original.call() always returns false + return false; + } + + private static boolean shouldCallOriginal(ChatComponent.ChatGraphicsAccess instance, int textTop, FormattedCharSequence text, GuiMessage.Line line) { + if (!(instance instanceof ChatComponent.ClickableTextOnlyGraphicsAccess clickableTextOnlyGraphicsAccess)) { + return true; + } + Minecraft minecraft = Minecraft.getInstance(); + if (!(minecraft.gui.screen() instanceof ChatScreen chatScreen)) { + return true; + } + if (!((IChatScreen) chatScreen).clientcommands_isTranslating()) { + return true; + } + if (!(clickableTextOnlyGraphicsAccess.output instanceof ActiveTextCollector.ClickableStyleFinder clickableStyleFinder)) { + return true; + } + Font font = minecraft.font; + ActiveTextCollector.Parameters parameters = clickableStyleFinder.defaultParameters(); + int leftX = TextAlignment.LEFT.calculateLeft(0, font, text); + GuiTextRenderState renderState = new GuiTextRenderState(font, text, parameters.pose(), leftX, textTop, ARGB.white(parameters.opacity()), 0, true, true, parameters.scissor()); + boolean[] found = {false}; + ActiveTextCollector.findElementUnderCursor(renderState, clickableStyleFinder.testX, clickableStyleFinder.testY, _ -> { + minecraft.keyboardHandler.setClipboard(line.parent().content().getString()); + found[0] = true; + }); + return !found[0]; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatScreenMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatScreenMixin.java new file mode 100644 index 000000000..194ae16f8 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatScreenMixin.java @@ -0,0 +1,34 @@ +package net.earthcomputer.clientcommands.mixin.commands.translate; + +import net.earthcomputer.clientcommands.command.TranslateCommand; +import net.earthcomputer.clientcommands.interfaces.IChatScreen; +import net.minecraft.client.gui.screens.ChatScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ChatScreen.class) +public abstract class ChatScreenMixin extends Screen implements IChatScreen { + + @Unique + private boolean isTranslating = false; + + protected ChatScreenMixin(Component title) { + super(title); + } + + @Inject(method = "onEdited", at = @At("TAIL")) + private void onEdited(String value, CallbackInfo ci) { + this.isTranslating = value.startsWith(Commands.COMMAND_PREFIX + TranslateCommand.LITERAL); + } + + @Override + public boolean clientcommands_isTranslating() { + return this.isTranslating; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/package-info.java b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/package-info.java new file mode 100644 index 000000000..88eedf01f --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package net.earthcomputer.clientcommands.mixin.commands.translate; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/resources/clientcommands.aw b/src/main/resources/clientcommands.aw index e220cb5c7..c6bd9c5bf 100644 --- a/src/main/resources/clientcommands.aw +++ b/src/main/resources/clientcommands.aw @@ -49,6 +49,12 @@ accessible field net/minecraft/client/renderer/ShaderManager$CompilationCache co # crelog accessible field net/minecraft/client/gui/screens/DisconnectedScreen parent Lnet/minecraft/client/gui/screens/Screen; +# ctranslate +accessible class net/minecraft/client/gui/components/ChatComponent$ClickableTextOnlyGraphicsAccess +accessible field net/minecraft/client/gui/components/ChatComponent$ClickableTextOnlyGraphicsAccess output Lnet/minecraft/client/gui/ActiveTextCollector; +accessible field net/minecraft/client/gui/ActiveTextCollector$ClickableStyleFinder testX I +accessible field net/minecraft/client/gui/ActiveTextCollector$ClickableStyleFinder testY I + # Game Options accessible field net/minecraft/client/OptionInstance value Ljava/lang/Object; diff --git a/src/main/resources/mixins.clientcommands.json b/src/main/resources/mixins.clientcommands.json index 0f4e72be7..bb3f16e0c 100644 --- a/src/main/resources/mixins.clientcommands.json +++ b/src/main/resources/mixins.clientcommands.json @@ -81,6 +81,8 @@ "commands.reply.ClientPacketListenerMixin", "commands.snap.MinecraftMixin", "commands.time.ClientClockManagerMixin", + "commands.translate.ChatComponent$1Mixin", + "commands.translate.ChatScreenMixin", "dataqueryhandler.ClientPacketListenerMixin", "events.ClientPacketListenerMixin", "events.GuiMixin", From cb84a3543746fef3c8260b58ff0c6fa7c16527e2 Mon Sep 17 00:00:00 2001 From: Frederik van der Els Date: Tue, 7 Jul 2026 21:26:49 +0200 Subject: [PATCH 2/3] Remove debug annotation --- .../mixin/commands/translate/ChatComponent$1Mixin.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatComponent$1Mixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatComponent$1Mixin.java index 061538162..5baccf313 100644 --- a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatComponent$1Mixin.java +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatComponent$1Mixin.java @@ -13,11 +13,9 @@ import net.minecraft.client.renderer.state.gui.GuiTextRenderState; import net.minecraft.util.ARGB; import net.minecraft.util.FormattedCharSequence; -import org.spongepowered.asm.mixin.Debug; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -@Debug(export = true) @Mixin(targets = "net/minecraft/client/gui/components/ChatComponent$1") public abstract class ChatComponent$1Mixin { @WrapOperation(method = "accept", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/components/ChatComponent$ChatGraphicsAccess;handleMessage(IFLnet/minecraft/util/FormattedCharSequence;)Z")) From 047a873cb61d189436dda6c39fca04e7ba20c96d Mon Sep 17 00:00:00 2001 From: Frederik van der Els Date: Tue, 21 Jul 2026 01:34:36 +0200 Subject: [PATCH 3/3] Address review comments --- .../command/TranslateCommand.java | 4 ++-- ...$1Mixin.java => ChatComponent_1Mixin.java} | 20 ++++++++++--------- .../commands/translate/ChatScreenMixin.java | 2 +- src/main/resources/mixins.clientcommands.json | 2 +- 4 files changed, 15 insertions(+), 13 deletions(-) rename src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/{ChatComponent$1Mixin.java => ChatComponent_1Mixin.java} (83%) diff --git a/src/main/java/net/earthcomputer/clientcommands/command/TranslateCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/TranslateCommand.java index faa0f6ccc..21b7c6e7f 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/TranslateCommand.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/TranslateCommand.java @@ -25,7 +25,7 @@ public class TranslateCommand { - public static final String LITERAL = "ctranslate"; + public static final String COMMAND_NAME = "ctranslate"; private static final SimpleCommandExceptionType UNKNOWN_ERROR_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.ctranslate.unknownError")); @@ -36,7 +36,7 @@ public class TranslateCommand { private static final Duration DURATION = Duration.ofSeconds(5); public static void register(CommandDispatcher dispatcher) { - dispatcher.register(literal(LITERAL) + dispatcher.register(literal(COMMAND_NAME) .then(argument("query", translationQuery()) .executes(ctx -> translate(ctx.getSource(), getTranslationQuery(ctx, "query"))))); } diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatComponent$1Mixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatComponent_1Mixin.java similarity index 83% rename from src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatComponent$1Mixin.java rename to src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatComponent_1Mixin.java index 5baccf313..3d056609c 100644 --- a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatComponent$1Mixin.java +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatComponent_1Mixin.java @@ -14,32 +14,34 @@ import net.minecraft.util.ARGB; import net.minecraft.util.FormattedCharSequence; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; @Mixin(targets = "net/minecraft/client/gui/components/ChatComponent$1") -public abstract class ChatComponent$1Mixin { +public abstract class ChatComponent_1Mixin { @WrapOperation(method = "accept", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/components/ChatComponent$ChatGraphicsAccess;handleMessage(IFLnet/minecraft/util/FormattedCharSequence;)Z")) private boolean copyText(ChatComponent.ChatGraphicsAccess instance, int textTop, float opacity, FormattedCharSequence text, Operation original, GuiMessage.Line line) { - if (shouldCallOriginal(instance, textTop, text, line)) { + if (!tryCopy(instance, textTop, text, line)) { return original.call(instance, textTop, opacity, text); } - // original.call() always returns false + // original.call() always returns false, so we return false here also return false; } - private static boolean shouldCallOriginal(ChatComponent.ChatGraphicsAccess instance, int textTop, FormattedCharSequence text, GuiMessage.Line line) { + @Unique + private static boolean tryCopy(ChatComponent.ChatGraphicsAccess instance, int textTop, FormattedCharSequence text, GuiMessage.Line line) { if (!(instance instanceof ChatComponent.ClickableTextOnlyGraphicsAccess clickableTextOnlyGraphicsAccess)) { - return true; + return false; } Minecraft minecraft = Minecraft.getInstance(); if (!(minecraft.gui.screen() instanceof ChatScreen chatScreen)) { - return true; + return false; } if (!((IChatScreen) chatScreen).clientcommands_isTranslating()) { - return true; + return false; } if (!(clickableTextOnlyGraphicsAccess.output instanceof ActiveTextCollector.ClickableStyleFinder clickableStyleFinder)) { - return true; + return false; } Font font = minecraft.font; ActiveTextCollector.Parameters parameters = clickableStyleFinder.defaultParameters(); @@ -50,6 +52,6 @@ private static boolean shouldCallOriginal(ChatComponent.ChatGraphicsAccess insta minecraft.keyboardHandler.setClipboard(line.parent().content().getString()); found[0] = true; }); - return !found[0]; + return found[0]; } } diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatScreenMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatScreenMixin.java index 194ae16f8..e6b9168a7 100644 --- a/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatScreenMixin.java +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/commands/translate/ChatScreenMixin.java @@ -24,7 +24,7 @@ protected ChatScreenMixin(Component title) { @Inject(method = "onEdited", at = @At("TAIL")) private void onEdited(String value, CallbackInfo ci) { - this.isTranslating = value.startsWith(Commands.COMMAND_PREFIX + TranslateCommand.LITERAL); + this.isTranslating = value.startsWith(Commands.COMMAND_PREFIX + TranslateCommand.COMMAND_NAME); } @Override diff --git a/src/main/resources/mixins.clientcommands.json b/src/main/resources/mixins.clientcommands.json index bb3f16e0c..2d4952421 100644 --- a/src/main/resources/mixins.clientcommands.json +++ b/src/main/resources/mixins.clientcommands.json @@ -81,7 +81,7 @@ "commands.reply.ClientPacketListenerMixin", "commands.snap.MinecraftMixin", "commands.time.ClientClockManagerMixin", - "commands.translate.ChatComponent$1Mixin", + "commands.translate.ChatComponent_1Mixin", "commands.translate.ChatScreenMixin", "dataqueryhandler.ClientPacketListenerMixin", "events.ClientPacketListenerMixin",