From 784231941661a3108549a1b5cd499bc5f7de2e46 Mon Sep 17 00:00:00 2001
From: nea <nea@nea.moe>
Date: Fri, 25 Aug 2023 14:18:43 +0200
Subject: Add better key binding support

---
 .../nea/firmament/mixins/MixinKeybindsScreen.java  |  53 ++++++++++
 .../moe/nea/firmament/features/FeatureManager.kt   |   2 +-
 .../firmament/features/inventory/SlotLocking.kt    |  16 ++-
 .../nea/firmament/gui/config/KeyBindingHandler.kt  | 113 +++++++++++++++++++++
 .../moe/nea/firmament/gui/config/ManagedConfig.kt  |  35 +++++--
 .../firmament/keybindings/FirmamentKeyBindings.kt  |  23 +++--
 .../nea/firmament/keybindings/SavedKeyBinding.kt   |  36 +++++++
 .../kotlin/moe/nea/firmament/util/ScreenUtil.kt    |   6 +-
 .../resources/assets/firmament/lang/en_us.json     |   3 +
 9 files changed, 261 insertions(+), 26 deletions(-)
 create mode 100644 src/main/java/moe/nea/firmament/mixins/MixinKeybindsScreen.java
 create mode 100644 src/main/kotlin/moe/nea/firmament/gui/config/KeyBindingHandler.kt
 create mode 100644 src/main/kotlin/moe/nea/firmament/keybindings/SavedKeyBinding.kt

(limited to 'src')

diff --git a/src/main/java/moe/nea/firmament/mixins/MixinKeybindsScreen.java b/src/main/java/moe/nea/firmament/mixins/MixinKeybindsScreen.java
new file mode 100644
index 0000000..23bc26c
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/MixinKeybindsScreen.java
@@ -0,0 +1,53 @@
+package moe.nea.firmament.mixins;
+
+import moe.nea.firmament.gui.config.ManagedConfig;
+import moe.nea.firmament.keybindings.FirmamentKeyBindings;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.option.ControlsListWidget;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.option.KeyBinding;
+import net.minecraft.text.Text;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Mutable;
+import org.spongepowered.asm.mixin.Shadow;
+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;
+
+@Mixin(ControlsListWidget.KeyBindingEntry.class)
+public class MixinKeybindsScreen {
+
+    @Mutable
+    @Shadow
+    @Final
+    private ButtonWidget editButton;
+
+    @Shadow
+    @Final
+    private KeyBinding binding;
+
+    @Shadow
+    @Final
+    private ButtonWidget resetButton;
+
+    @ModifyArg(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/widget/ButtonWidget;builder(Lnet/minecraft/text/Text;Lnet/minecraft/client/gui/widget/ButtonWidget$PressAction;)Lnet/minecraft/client/gui/widget/ButtonWidget$Builder;"))
+    public ButtonWidget.PressAction onInit(ButtonWidget.PressAction action) {
+        ManagedConfig config = FirmamentKeyBindings.INSTANCE.getKeyBindings().get(binding);
+        if (config == null) return action;
+        return button -> {
+            config.showConfigEditor(MinecraftClient.getInstance().currentScreen);
+        };
+    }
+
+    @Inject(method = "update", at = @At("HEAD"), cancellable = true)
+    public void onUpdate(CallbackInfo ci) {
+        ManagedConfig config = FirmamentKeyBindings.INSTANCE.getKeyBindings().get(binding);
+        if (config == null) return;
+        resetButton.active = false;
+        editButton.setMessage(Text.translatable("firmament.keybinding.external"));
+        ci.cancel();
+    }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
index 0c8acb9..facb821 100644
--- a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
@@ -12,7 +12,6 @@ import moe.nea.firmament.Firmament
 import moe.nea.firmament.features.chat.ChatLinks
 import moe.nea.firmament.features.debug.DebugView
 import moe.nea.firmament.features.debug.DeveloperFeatures
-import moe.nea.firmament.features.fishing.FishingWarning
 import moe.nea.firmament.features.fixes.Fixes
 import moe.nea.firmament.features.inventory.CraftingOverlay
 import moe.nea.firmament.features.inventory.SaveCursorPosition
@@ -54,6 +53,7 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
                 loadFeature(DeveloperFeatures)
                 loadFeature(DebugView)
             }
+            allFeatures.forEach { it.config }
             hasAutoloaded = true
         }
     }
diff --git a/src/main/kotlin/moe/nea/firmament/features/inventory/SlotLocking.kt b/src/main/kotlin/moe/nea/firmament/features/inventory/SlotLocking.kt
index f6a197c..ec86341 100644
--- a/src/main/kotlin/moe/nea/firmament/features/inventory/SlotLocking.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/inventory/SlotLocking.kt
@@ -8,17 +8,17 @@ package moe.nea.firmament.features.inventory
 
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.serializer
-import net.minecraft.client.gui.DrawContext
-import net.minecraft.entity.player.PlayerInventory
 import moe.nea.firmament.events.HandledScreenKeyPressedEvent
 import moe.nea.firmament.events.IsSlotProtectedEvent
 import moe.nea.firmament.events.SlotRenderEvents
 import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.keybindings.FirmamentKeyBindings
+import moe.nea.firmament.gui.config.ManagedConfig
 import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
 import moe.nea.firmament.util.CommonSoundEffects
 import moe.nea.firmament.util.MC
 import moe.nea.firmament.util.data.ProfileSpecificDataHolder
+import net.minecraft.entity.player.PlayerInventory
+import org.lwjgl.glfw.GLFW
 
 object SlotLocking : FirmamentFeature {
     override val identifier: String
@@ -29,13 +29,19 @@ object SlotLocking : FirmamentFeature {
         val lockedSlots: MutableSet<Int> = mutableSetOf(),
     )
 
+    object TConfig : ManagedConfig(identifier) {
+        val lock by keyBinding("lock") { GLFW.GLFW_KEY_L }
+    }
+
+    override val config: TConfig
+            get() = TConfig
+
     object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "locked-slots", ::Data)
 
-    val keyBinding by FirmamentKeyBindings::SLOT_LOCKING
     val lockedSlots get() = DConfig.data?.lockedSlots
     override fun onLoad() {
         HandledScreenKeyPressedEvent.subscribe {
-            if (!it.matches(keyBinding)) return@subscribe
+            if (!it.matches(TConfig.lock)) return@subscribe
             val inventory = MC.handledScreen ?: return@subscribe
             inventory as AccessorHandledScreen
 
diff --git a/src/main/kotlin/moe/nea/firmament/gui/config/KeyBindingHandler.kt b/src/main/kotlin/moe/nea/firmament/gui/config/KeyBindingHandler.kt
new file mode 100644
index 0000000..5a4e3af
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/gui/config/KeyBindingHandler.kt
@@ -0,0 +1,113 @@
+package moe.nea.firmament.gui.config
+
+import io.github.cottonmc.cotton.gui.widget.WButton
+import io.github.cottonmc.cotton.gui.widget.data.InputResult
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.decodeFromJsonElement
+import kotlinx.serialization.json.encodeToJsonElement
+import moe.nea.firmament.keybindings.FirmamentKeyBindings
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import net.minecraft.client.util.InputUtil
+import net.minecraft.text.Text
+import net.minecraft.util.Formatting
+import org.lwjgl.glfw.GLFW
+
+class KeyBindingHandler(name: String, managedConfig: ManagedConfig) : ManagedConfig.OptionHandler<SavedKeyBinding> {
+    init {
+        FirmamentKeyBindings.registerKeyBinding(name, managedConfig)
+    }
+
+    override fun toJson(element: SavedKeyBinding): JsonElement? {
+        return Json.encodeToJsonElement(element)
+    }
+
+    override fun fromJson(element: JsonElement): SavedKeyBinding {
+        return Json.decodeFromJsonElement(element)
+    }
+
+    override fun emitGuiElements(opt: ManagedOption<SavedKeyBinding>, guiAppender: GuiAppender) {
+        var editing = false
+        var lastPressed = 0
+        var lastPressedNonModifier = 0
+        var updateButton: (() -> Unit)? = null
+        val button = object : WButton() {
+            override fun onKeyPressed(ch: Int, key: Int, modifiers: Int): InputResult {
+                if (!editing) {
+                    return super.onKeyPressed(ch, key, modifiers)
+                }
+                if (ch == GLFW.GLFW_KEY_ESCAPE) {
+                    lastPressedNonModifier = 0
+                    editing = false
+                    lastPressed = 0
+                    updateButton!!()
+                    return InputResult.PROCESSED
+                }
+                if (ch == GLFW.GLFW_KEY_LEFT_SHIFT || ch == GLFW.GLFW_KEY_RIGHT_SHIFT
+                    || ch == GLFW.GLFW_KEY_LEFT_ALT || ch == GLFW.GLFW_KEY_RIGHT_ALT
+                    || ch == GLFW.GLFW_KEY_LEFT_CONTROL || ch == GLFW.GLFW_KEY_RIGHT_CONTROL
+                ) {
+                    lastPressed = ch
+                } else {
+                    opt.value = SavedKeyBinding(
+                        ch, modifiers
+                    )
+                    editing = false
+                    lastPressed = 0
+                    lastPressedNonModifier = 0
+                }
+                updateButton!!()
+                return InputResult.PROCESSED
+            }
+
+            override fun onFocusLost() {
+                super.onFocusLost()
+                lastPressedNonModifier = 0
+                editing = false
+                lastPressed = 0
+                updateButton!!()
+            }
+
+            override fun onKeyReleased(ch: Int, key: Int, modifiers: Int): InputResult {
+                if (!editing)
+                    return super.onKeyReleased(ch, key, modifiers)
+                if (lastPressedNonModifier == ch || (lastPressedNonModifier == 0 && ch == lastPressed)) {
+                    opt.value = SavedKeyBinding(
+                        ch, modifiers
+                    )
+                    editing = false
+                    lastPressed = 0
+                    lastPressedNonModifier = 0
+                }
+                updateButton!!()
+                return InputResult.PROCESSED
+            }
+        }
+
+        fun updateLabel() {
+            val stroke = Text.literal("")
+            if (opt.value.shift) {
+                stroke.append("SHIFT + ") // TODO: translations?
+            }
+            if (opt.value.alt) {
+                stroke.append("ALT + ")
+            }
+            if (opt.value.ctrl) {
+                stroke.append("CTRL + ")
+            }
+            stroke.append(InputUtil.Type.KEYSYM.createFromCode(opt.value.keyCode).localizedText)
+            if (editing)
+                stroke.styled { it.withColor(Formatting.YELLOW) }
+            button.setLabel(stroke)
+        }
+        updateButton = ::updateLabel
+        updateButton()
+        button.setOnClick {
+            editing = true
+            button.requestFocus()
+            updateButton()
+        }
+        guiAppender.appendLabeledRow(opt.labelText, button)
+    }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/gui/config/ManagedConfig.kt b/src/main/kotlin/moe/nea/firmament/gui/config/ManagedConfig.kt
index 56aca46..8743293 100644
--- a/src/main/kotlin/moe/nea/firmament/gui/config/ManagedConfig.kt
+++ b/src/main/kotlin/moe/nea/firmament/gui/config/ManagedConfig.kt
@@ -14,21 +14,22 @@ import io.github.cottonmc.cotton.gui.widget.WLabel
 import io.github.cottonmc.cotton.gui.widget.data.Axis
 import io.github.cottonmc.cotton.gui.widget.data.Insets
 import io.github.cottonmc.cotton.gui.widget.data.VerticalAlignment
-import moe.nea.jarvis.api.Point
 import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.JsonElement
 import kotlinx.serialization.json.JsonObject
-import kotlin.io.path.createDirectories
-import kotlin.io.path.readText
-import kotlin.io.path.writeText
-import kotlin.time.Duration
-import net.minecraft.client.gui.screen.Screen
-import net.minecraft.text.Text
 import moe.nea.firmament.Firmament
 import moe.nea.firmament.gui.WTightScrollPanel
+import moe.nea.firmament.keybindings.SavedKeyBinding
 import moe.nea.firmament.util.MC
 import moe.nea.firmament.util.ScreenUtil.setScreenLater
+import moe.nea.jarvis.api.Point
+import net.minecraft.client.gui.screen.Screen
+import net.minecraft.text.Text
+import kotlin.io.path.createDirectories
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
+import kotlin.time.Duration
 
 abstract class ManagedConfig(override val name: String) : ManagedConfigElement() {
 
@@ -106,6 +107,18 @@ abstract class ManagedConfig(override val name: String) : ManagedConfigElement()
         }, HudMetaHandler(this, label, width, height))
     }
 
+    protected fun keyBinding(
+        propertyName: String,
+        default: () -> Int,
+    ): ManagedOption<SavedKeyBinding> = keyBindingWithDefaultModifiers(propertyName) { SavedKeyBinding(default()) }
+
+    protected fun keyBindingWithDefaultModifiers(
+        propertyName: String,
+        default: () -> SavedKeyBinding,
+    ): ManagedOption<SavedKeyBinding> {
+        return option(propertyName, default, KeyBindingHandler("firmament.config.${name}.${propertyName}", this))
+    }
+
     protected fun integer(
         propertyName: String,
         min: Int,
@@ -125,7 +138,7 @@ abstract class ManagedConfig(override val name: String) : ManagedConfigElement()
 
 
     fun reloadGui() {
-        latestGuiAppender?.reloadables?.forEach {it() }
+        latestGuiAppender?.reloadables?.forEach { it() }
     }
 
     fun getConfigEditor(parent: Screen? = null): CottonClientScreen {
@@ -137,7 +150,11 @@ abstract class ManagedConfig(override val name: String) : ManagedConfigElement()
         guiapp.appendFullRow(WBox(Axis.HORIZONTAL).also {
             it.add(WButton(Text.literal("←")).also {
                 it.setOnClick {
-                    AllConfigsGui.showAllGuis()
+                    if (parent != null) {
+                        setScreenLater(parent)
+                    } else {
+                        AllConfigsGui.showAllGuis()
+                    }
                 }
             })
             it.add(WLabel(Text.translatable("firmament.config.${name}")).also {
diff --git a/src/main/kotlin/moe/nea/firmament/keybindings/FirmamentKeyBindings.kt b/src/main/kotlin/moe/nea/firmament/keybindings/FirmamentKeyBindings.kt
index a7f25c0..770f4f6 100644
--- a/src/main/kotlin/moe/nea/firmament/keybindings/FirmamentKeyBindings.kt
+++ b/src/main/kotlin/moe/nea/firmament/keybindings/FirmamentKeyBindings.kt
@@ -7,17 +7,24 @@
 package moe.nea.firmament.keybindings
 
 import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper
-import org.lwjgl.glfw.GLFW
 import net.minecraft.client.option.KeyBinding
 import net.minecraft.client.util.InputUtil
+import moe.nea.firmament.features.inventory.SlotLocking
+import moe.nea.firmament.gui.config.ManagedConfig
 
 object FirmamentKeyBindings {
-    val SLOT_LOCKING = KeyBindingHelper.registerKeyBinding(
-        KeyBinding(
-            "firmament.key.slotlocking",
-            InputUtil.Type.KEYSYM,
-            GLFW.GLFW_KEY_L,
-            "firmament.key.category"
+    fun registerKeyBinding(name: String, config: ManagedConfig) {
+        val vanillaKeyBinding = KeyBindingHelper.registerKeyBinding(
+            KeyBinding(
+                name,
+                InputUtil.Type.KEYSYM,
+                -1,
+                "firmament.key.category"
+            )
         )
-    )
+        keyBindings[vanillaKeyBinding] = config
+    }
+
+    val keyBindings = mutableMapOf<KeyBinding, ManagedConfig>()
+
 }
diff --git a/src/main/kotlin/moe/nea/firmament/keybindings/SavedKeyBinding.kt b/src/main/kotlin/moe/nea/firmament/keybindings/SavedKeyBinding.kt
new file mode 100644
index 0000000..f73eb49
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/keybindings/SavedKeyBinding.kt
@@ -0,0 +1,36 @@
+package moe.nea.firmament.keybindings
+
+import kotlinx.serialization.Serializable
+import org.lwjgl.glfw.GLFW
+
+@Serializable
+data class SavedKeyBinding(
+    val keyCode: Int,
+    val shift: Boolean = false,
+    val ctrl: Boolean = false,
+    val alt: Boolean = false,
+) : IKeyBinding {
+    constructor(keyCode: Int, mods: Triple<Boolean, Boolean, Boolean>) : this(
+        keyCode,
+        mods.first && keyCode != GLFW.GLFW_KEY_LEFT_SHIFT && keyCode != GLFW.GLFW_KEY_RIGHT_SHIFT,
+        mods.second && keyCode != GLFW.GLFW_KEY_LEFT_CONTROL && keyCode != GLFW.GLFW_KEY_RIGHT_CONTROL,
+        mods.third && keyCode != GLFW.GLFW_KEY_LEFT_ALT && keyCode != GLFW.GLFW_KEY_RIGHT_ALT,
+    )
+
+    constructor(keyCode: Int, mods: Int) : this(keyCode, getMods(mods))
+
+    companion object {
+        fun getMods(modifiers: Int): Triple<Boolean, Boolean, Boolean> {
+            return Triple(
+                modifiers and GLFW.GLFW_MOD_SHIFT != 0,
+                modifiers and GLFW.GLFW_MOD_CONTROL != 0,
+                modifiers and GLFW.GLFW_MOD_ALT != 0
+            )
+        }
+    }
+
+    override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+        return keyCode == this.keyCode && getMods(modifiers) == Triple(shift, ctrl, alt)
+    }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/util/ScreenUtil.kt b/src/main/kotlin/moe/nea/firmament/util/ScreenUtil.kt
index 39d4541..25c603a 100644
--- a/src/main/kotlin/moe/nea/firmament/util/ScreenUtil.kt
+++ b/src/main/kotlin/moe/nea/firmament/util/ScreenUtil.kt
@@ -6,10 +6,10 @@
 
 package moe.nea.firmament.util
 
-import moe.nea.firmament.Firmament
 import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
 import net.minecraft.client.MinecraftClient
 import net.minecraft.client.gui.screen.Screen
+import moe.nea.firmament.Firmament
 
 object ScreenUtil {
     init {
@@ -29,10 +29,10 @@ object ScreenUtil {
 
     private var nextOpenedGui: Screen? = null
 
-    fun setScreenLater(nextScreen: Screen) {
+    fun setScreenLater(nextScreen: Screen?) {
         val nog = nextOpenedGui
         if (nog != null) {
-            Firmament.logger.warn("Setting screen ${nextScreen::class.qualifiedName} to be opened later, but ${nog::class.qualifiedName} is already queued.")
+            Firmament.logger.warn("Setting screen ${if (nextScreen == null) "null" else nextScreen::class.qualifiedName} to be opened later, but ${nog::class.qualifiedName} is already queued.")
             return
         }
         nextOpenedGui = nextScreen
diff --git a/src/main/resources/assets/firmament/lang/en_us.json b/src/main/resources/assets/firmament/lang/en_us.json
index 5df94a7..ad799e2 100644
--- a/src/main/resources/assets/firmament/lang/en_us.json
+++ b/src/main/resources/assets/firmament/lang/en_us.json
@@ -79,6 +79,9 @@
     "firmament.config.chat-links.allowed-hosts": "Allowed Image Hosts",
     "firmament.config.chat-links.position": "Chat Image Preview",
     "firmament.hud.edit": "Edit %s",
+    "firmament.keybinding.external": "External",
+    "firmament.config.slot-locking": "Slot Locking",
+    "firmament.config.slot-locking.lock": "Lock Slot",
     "firmament.config.custom-skyblock-textures": "Custom SkyBlock Item Textures",
     "firmament.config.custom-skyblock-textures.cache-duration": "Model Cache Duration",
     "firmament.config.custom-skyblock-textures.enabled": "Enable Custom Item Textures",
-- 
cgit