diff --git a/src/tel/eden/mod/EdenModClient.java b/src/tel/eden/mod/EdenModClient.java index 75fe081..b87dafc 100644 --- a/src/tel/eden/mod/EdenModClient.java +++ b/src/tel/eden/mod/EdenModClient.java @@ -311,14 +311,24 @@ public boolean isOpenEmotePickerMouseBound() { return openEmotePickerKey.saveString().startsWith("key.mouse."); } + public boolean matchesOpenEmotePickerKey(net.minecraft.client.input.KeyEvent event) { + return openEmotePickerKey.matches(event); + } + + /** + * If the picker shares chat's gameplay keybind, vanilla opens chat before the chat + * screen can inspect the triggering press. Detect that held key during chat init + * so the picker still opens without requiring a second press. + */ public boolean shouldOpenEmotePickerOnChatOpen() { Minecraft mc = Minecraft.getInstance(); - if (isOpenEmotePickerMouseBound() || mc.options == null) { + if (!shouldAllowGameplayEmotePickerOpen() || isOpenEmotePickerMouseBound() || mc.options == null) { return false; } - return openEmotePickerKey.saveString().equals(mc.options.keyChat.saveString()) && openEmotePickerKey.isDown(); + return edenmod$matchesGameplayChatKeybind(mc.options.keyChat) || edenmod$matchesGameplayChatKeybind(mc.options.keyCommand); } + /** Queue the emote picker for the next chat screen, opening chat if needed. */ public void openCenteredEmotePicker() { Minecraft mc = Minecraft.getInstance(); requestCenteredEmotePicker(); @@ -339,6 +349,24 @@ public java.util.List knownPendingAspects() { return knownPendingAspects; } + private boolean shouldAllowGameplayEmotePickerOpen() { + if (!config.emotePickerOpenFromGameplay) { + return false; + } + return switch (config.chatEmoteToolsMode) { + case UI, UI_AND_AUTO -> true; + case AUTO, NONE -> false; + }; + } + + private boolean shouldOpenEmotePickerFromGameplay(Minecraft client) { + return shouldAllowGameplayEmotePickerOpen() && client.screen == null; + } + + private boolean edenmod$matchesGameplayChatKeybind(KeyMapping vanillaKey) { + return openEmotePickerKey.saveString().equals(vanillaKey.saveString()) && openEmotePickerKey.isDown(); + } + /** The error from the last aspects-pending reply, or {@code null} if it succeeded. */ public String pendingAspectsError() { return pendingAspectsError; @@ -555,10 +583,12 @@ private void onClientTick(Minecraft client) { } } while (openEmotePickerKey.consumeClick()) { - if (client.screen instanceof ChatScreen && isOpenEmotePickerMouseBound()) { - continue; + // ChatScreenMixin handles picker input while chat is already open. Only the + // gameplay-entry path goes through this tick hook, keeping the hot path to one + // cheap screen/config check and preventing stale queued clicks from replaying. + if (shouldOpenEmotePickerFromGameplay(client)) { + openCenteredEmotePicker(); } - openCenteredEmotePicker(); } pollCommandKeybinds(client); if (pendingUpdateNotification && client.player != null) { diff --git a/src/tel/eden/mod/config/BridgeConfig.java b/src/tel/eden/mod/config/BridgeConfig.java index cfdb003..371fc19 100644 --- a/src/tel/eden/mod/config/BridgeConfig.java +++ b/src/tel/eden/mod/config/BridgeConfig.java @@ -196,6 +196,13 @@ public String label() { /** Which chat emote tools are enabled: inline/picker UI, autocomplete, both, or none. */ public ChatEmoteToolsMode chatEmoteToolsMode = ChatEmoteToolsMode.UI_AND_AUTO; + /** + * Whether the emote-picker keybind may open chat directly from gameplay when no + * other screen is open. When disabled, the binding only works while chat is + * already focused. + */ + public boolean emotePickerOpenFromGameplay = true; + /** Visible emote-picker columns in the chat overlay. */ public int emotePickerColumns = 5; diff --git a/src/tel/eden/mod/gui/BridgeConfigScreen.java b/src/tel/eden/mod/gui/BridgeConfigScreen.java index be11986..359fd03 100644 --- a/src/tel/eden/mod/gui/BridgeConfigScreen.java +++ b/src/tel/eden/mod/gui/BridgeConfigScreen.java @@ -84,6 +84,7 @@ protected void init() { addToggleRow("My login/logout messages", () -> config.announceSelfPresence, v -> config.announceSelfPresence = v, "On", "Off", true); addToggleRow("Party feed", () -> config.partyAnnounce, v -> config.partyAnnounce = v, "On", "Off", true); addCycleRow("Chat emote tools", () -> config.chatEmoteToolsMode.label(), () -> config.chatEmoteToolsMode = nextChatEmoteToolsMode(config.chatEmoteToolsMode), () -> config.chatEmoteToolsMode = BridgeConfig.ChatEmoteToolsMode.UI_AND_AUTO); + addToggleRow("Allow emote picker outside chat", () -> config.emotePickerOpenFromGameplay, v -> config.emotePickerOpenFromGameplay = v, "Allowed", "Chat only", true); addCycleRow("Game messages", () -> shortGameModeLabel(config.gameDisplayMode), () -> config.gameDisplayMode = nextGameMode(config.gameDisplayMode), () -> config.gameDisplayMode = BridgeConfig.GameDisplayMode.ALL); PreviewSizeSlider slider = new PreviewSizeSlider(CONTROL_W, 20); addSliderRow("Image preview size", slider, slider::syncFromConfig, () -> config.imagePreviewSize = 40); diff --git a/src/tel/eden/mod/mixin/ChatScreenMixin.java b/src/tel/eden/mod/mixin/ChatScreenMixin.java index 9444ba4..6bf0f5a 100644 --- a/src/tel/eden/mod/mixin/ChatScreenMixin.java +++ b/src/tel/eden/mod/mixin/ChatScreenMixin.java @@ -143,6 +143,9 @@ public abstract class ChatScreenMixin { @Unique private int edenmod$sliderPreviewRows = -1; + @Unique + private boolean edenmod$suppressPickerKeyChars; + @Inject(method = "init", at = @At("TAIL")) private void edenmod$addEmoteFormatter(CallbackInfo ci) { input.addFormatter((text, cursor) -> { @@ -152,6 +155,8 @@ public abstract class ChatScreenMixin { FormattedCharSequence formatted = ChatEmoteFormatter.format(text); return formatted; }); + // Shared chat/picker bindings open the screen before this mixin sees the original + // key press, so init needs to revive the queued picker request for that case. if (EdenModClient.instance().shouldOpenEmotePickerOnChatOpen()) { EdenModClient.instance().requestCenteredEmotePicker(); } @@ -164,6 +169,14 @@ public abstract class ChatScreenMixin { edenmod$resetOverlayState(); return; } + if (edenmod$isChatEmoteUiVisible() && !edenmod$pickerOpen && EdenModClient.instance().matchesOpenEmotePickerKey(event)) { + double[] mouse = edenmod$currentMousePosition(); + edenmod$openPickerForMode(mouse[0], mouse[1]); + edenmod$suppressPickerKeyChars = true; + input.setEditable(false); + cir.setReturnValue(true); + return; + } if (edenmod$isChatEmoteUiVisible() && edenmod$pickerOpen && event.key() == GLFW.GLFW_KEY_ESCAPE) { if (edenmod$pickerSettingsOpen) { edenmod$pickerSettingsOpen = false; @@ -261,6 +274,10 @@ public abstract class ChatScreenMixin { @Inject(method = "render", at = @At("TAIL")) private void edenmod$renderChatOverlays(GuiGraphics graphics, int mouseX, int mouseY, float delta, CallbackInfo ci) { + if (edenmod$suppressPickerKeyChars) { + input.setEditable(true); + edenmod$suppressPickerKeyChars = false; + } // Hovering an attack-timer head shows that player's IGN (works regardless of the // emote-tool settings, like the timer click). AttackTimerMenu.renderGoerTooltip(graphics, mouseX, mouseY); @@ -989,6 +1006,10 @@ public abstract class ChatScreenMixin { @Unique private void edenmod$resetOverlayState() { + if (edenmod$suppressPickerKeyChars) { + input.setEditable(true); + edenmod$suppressPickerKeyChars = false; + } edenmod$pickerOpen = false; edenmod$pickerSettingsOpen = false; edenmod$draggingSlider = null;