feat: Add macro editing UI

This commit is contained in:
Linnea Gräf
2025-06-04 18:45:39 +02:00
parent 9c32c6e824
commit 3695589a47
14 changed files with 379 additions and 47 deletions

View File

@@ -10,6 +10,7 @@ import moe.nea.firmament.events.WorldKeyboardEvent
import moe.nea.firmament.keybindings.SavedKeyBinding
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.tr
object ComboProcessor {
@@ -22,18 +23,13 @@ object ComboProcessor {
var isInputting = false
var lastInput = TimeMark.farPast()
val breadCrumbs = mutableListOf<SavedKeyBinding>()
// TODO: keep breadcrumbs
init {
val f = SavedKeyBinding(InputUtil.GLFW_KEY_F)
val one = SavedKeyBinding(InputUtil.GLFW_KEY_1)
val two = SavedKeyBinding(InputUtil.GLFW_KEY_2)
setActions(
listOf(
ComboKeyAction(CommandAction("wardrobe"), listOf(f, one)),
ComboKeyAction(CommandAction("equipment"), listOf(f, two)),
)
MacroData.DConfig.data.comboActions
)
}
@@ -68,10 +64,24 @@ object ComboProcessor {
0F
)
val breadCrumbText = breadCrumbs.joinToString(" > ")
event.context.drawText(MC.font, breadCrumbText, 0, 0, -1, true)
event.context.drawText(
MC.font,
tr("firmament.combo.active", "Current Combo: ").append(breadCrumbText),
0,
0,
-1,
true
)
event.context.matrices.translate(0F, MC.font.fontHeight + 2F, 0F)
for ((key, value) in activeTrie.nodes) {
event.context.drawText(MC.font, Text.literal("$breadCrumbText > $key: ").append(value.label), 0, 0, -1, true)
event.context.drawText(
MC.font,
Text.literal("$breadCrumbText > $key: ").append(value.label),
0,
0,
-1,
true
)
event.context.matrices.translate(0F, MC.font.fontHeight + 1F, 0F)
}
event.context.matrices.pop()

View File

@@ -1,14 +1,19 @@
package moe.nea.firmament.features.macros
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.minecraft.text.Text
import moe.nea.firmament.util.MC
interface HotkeyAction {
@Serializable
sealed interface HotkeyAction {
// TODO: execute
val label: Text
fun execute()
}
@Serializable
@SerialName("command")
data class CommandAction(val command: String) : HotkeyAction {
override val label: Text
get() = Text.literal("/$command")

View File

@@ -1,7 +1,9 @@
package moe.nea.firmament.features.macros
import kotlinx.serialization.Serializable
import net.minecraft.text.Text
import moe.nea.firmament.keybindings.SavedKeyBinding
import moe.nea.firmament.util.ErrorUtil
sealed interface KeyComboTrie {
val label: Text
@@ -13,19 +15,27 @@ sealed interface KeyComboTrie {
val root = Branch(mutableMapOf())
for (combo in combos) {
var p = root
require(combo.keys.isNotEmpty())
if (combo.keys.isEmpty()) {
ErrorUtil.softUserError("Key Combo for ${combo.action.label.string} is empty")
continue
}
for ((index, key) in combo.keys.withIndex()) {
val m = (p.nodes as MutableMap)
if (index == combo.keys.lastIndex) {
if (key in m)
error("Overlapping actions found for ${combo.keys} (another action ${m[key]} already exists).")
if (key in m) {
ErrorUtil.softUserError("Overlapping actions found for ${combo.keys.joinToString(" > ")} (another action ${m[key]} already exists).")
break
}
m[key] = Leaf(combo.action)
} else {
val c = m.getOrPut(key) { Branch(mutableMapOf()) }
if (c !is Branch)
error("Overlapping actions found for ${combo.keys} (final node exists at index $index) through another action already")
p = c
if (c !is Branch) {
ErrorUtil.softUserError("Overlapping actions found for ${combo.keys} (final node exists at index $index) through another action already")
break
} else {
p = c
}
}
}
}
@@ -35,6 +45,7 @@ sealed interface KeyComboTrie {
}
@Serializable
data class ComboKeyAction(
val action: HotkeyAction,
val keys: List<SavedKeyBinding>,

View File

@@ -0,0 +1,11 @@
package moe.nea.firmament.features.macros
import kotlinx.serialization.Serializable
import moe.nea.firmament.util.data.DataHolder
@Serializable
data class MacroData(
var comboActions: List<ComboKeyAction> = listOf(),
){
object DConfig : DataHolder<MacroData>(kotlinx.serialization.serializer(), "macros", ::MacroData)
}

View File

@@ -0,0 +1,161 @@
package moe.nea.firmament.features.macros
import io.github.notenoughupdates.moulconfig.gui.CloseEventListener
import io.github.notenoughupdates.moulconfig.observer.ObservableList
import io.github.notenoughupdates.moulconfig.xml.Bind
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.gui.config.AllConfigsGui.toObservableList
import moe.nea.firmament.gui.config.KeyBindingStateManager
import moe.nea.firmament.keybindings.SavedKeyBinding
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.MoulConfigUtils
import moe.nea.firmament.util.ScreenUtil
class MacroUI {
companion object {
@Subscribe
fun onCommands(event: CommandEvent.SubCommand) {
// TODO: add button in config
event.subcommand("macros") {
thenExecute {
ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen("config/macros/index", MacroUI(), null))
}
}
}
}
@field:Bind("combos")
val combos = Combos()
class Combos {
@field:Bind("actions")
val actions: ObservableList<ActionEditor> = ObservableList(
MacroData.DConfig.data.comboActions.mapTo(mutableListOf()) {
ActionEditor(it, this)
}
)
var dontSave = false
@Bind
fun beforeClose(): CloseEventListener.CloseAction {
if (!dontSave)
save()
return CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE
}
@Bind
fun addCommand() {
actions.add(
ActionEditor(
ComboKeyAction(
CommandAction("ac Hello from a Firmament Hotkey"),
listOf()
),
this
)
)
}
@Bind
fun discard() {
dontSave = true
MC.screen?.close()
}
@Bind
fun saveAndClose() {
save()
MC.screen?.close()
}
@Bind
fun save() {
MacroData.DConfig.data.comboActions = actions.map { it.asSaveable() }
MacroData.DConfig.markDirty()
ComboProcessor.setActions(MacroData.DConfig.data.comboActions) // TODO: automatically reload those from the config on startup
}
}
class KeyBindingEditor(var binding: SavedKeyBinding, val parent: ActionEditor) {
val sm = KeyBindingStateManager(
{ binding },
{ binding = it },
::blur,
::requestFocus
)
@field:Bind
val button = sm.createButton()
init {
sm.updateLabel()
}
fun blur() {
button.blur()
}
@Bind
fun delete() {
parent.combo.removeIf { it === this }
parent.combo.update()
}
fun requestFocus() {
button.requestFocus()
}
}
class ActionEditor(val action: ComboKeyAction, val parent: MacroUI.Combos) {
fun asSaveable(): ComboKeyAction {
return ComboKeyAction(
CommandAction(command),
combo.map { it.binding }
)
}
@field:Bind("command")
var command: String = (action.action as CommandAction).command
@field:Bind("combo")
val combo = action.keys.map { KeyBindingEditor(it, this) }.toObservableList()
@Bind
fun formattedCombo() =
combo.joinToString(" > ") { it.binding.toString() }
@Bind
fun addStep() {
combo.add(KeyBindingEditor(SavedKeyBinding.unbound(), this))
}
@Bind
fun back() {
MC.screen?.close()
}
@Bind
fun delete() {
parent.actions.removeIf { it === this }
parent.actions.update()
}
@Bind
fun edit() {
MC.screen = MoulConfigUtils.loadScreen("config/macros/editor", this, MC.screen)
}
}
}
private fun <T> ObservableList<T>.setAll(ts: Collection<T>) {
val observer = this.observer
this.clear()
this.addAll(ts)
this.observer = observer
this.update()
}

View File

@@ -40,34 +40,7 @@ class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) :
{ button.blur() },
{ button.requestFocus() }
)
button = object : FirmButtonComponent(
TextComponent(
IMinecraft.instance.defaultFontRenderer,
{ sm.label.string },
130,
TextComponent.TextAlignment.LEFT,
false,
false
), action = {
sm.onClick()
}) {
override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean {
if (event is KeyboardEvent.KeyPressed) {
return sm.keyboardEvent(event.keycode, event.pressed)
}
return super.keyboardEvent(event, context)
}
override fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> {
if (sm.editing) return activeBg
return super.getBackground(context)
}
override fun onLostFocus() {
sm.onLostFocus()
}
}
button = sm.createButton()
sm.updateLabel()
return button
}

View File

@@ -1,8 +1,15 @@
package moe.nea.firmament.gui.config
import io.github.notenoughupdates.moulconfig.common.IMinecraft
import io.github.notenoughupdates.moulconfig.common.MyResourceLocation
import io.github.notenoughupdates.moulconfig.deps.libninepatch.NinePatch
import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent
import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
import org.lwjgl.glfw.GLFW
import net.minecraft.text.Text
import net.minecraft.util.Formatting
import moe.nea.firmament.gui.FirmButtonComponent
import moe.nea.firmament.keybindings.SavedKeyBinding
class KeyBindingStateManager(
@@ -51,9 +58,11 @@ class KeyBindingStateManager(
) {
lastPressed = ch
} else {
setValue(SavedKeyBinding(
ch, modifiers
))
setValue(
SavedKeyBinding(
ch, modifiers
)
)
editing = false
blur()
lastPressed = 0
@@ -104,5 +113,34 @@ class KeyBindingStateManager(
label = stroke
}
fun createButton(): FirmButtonComponent {
return object : FirmButtonComponent(
TextComponent(
IMinecraft.instance.defaultFontRenderer,
{ this@KeyBindingStateManager.label.string },
130,
TextComponent.TextAlignment.LEFT,
false,
false
), action = {
this@KeyBindingStateManager.onClick()
}) {
override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean {
if (event is KeyboardEvent.KeyPressed) {
return this@KeyBindingStateManager.keyboardEvent(event.keycode, event.pressed)
}
return super.keyboardEvent(event, context)
}
override fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> {
if (this@KeyBindingStateManager.editing) return activeBg
return super.getBackground(context)
}
override fun onLostFocus() {
this@KeyBindingStateManager.onLostFocus()
}
}
}
}

View File

@@ -57,7 +57,9 @@ data class SavedKeyBinding(
fun isShiftDown() = InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_SHIFT)
|| InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_SHIFT)
}
fun unbound(): SavedKeyBinding =
SavedKeyBinding(GLFW.GLFW_KEY_UNKNOWN)
}
fun isPressed(atLeast: Boolean = false): Boolean {
if (!isBound) return false

View File

@@ -75,4 +75,7 @@ object ErrorUtil {
return nullable
}
fun softUserError(string: String) {
MC.sendChat(tr("frimanet.usererror", "Firmament encountered a user caused error: $string"))
}
}

View File

@@ -1,6 +1,7 @@
package moe.nea.firmament.util
import io.github.moulberry.repo.data.Coordinate
import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
import java.util.concurrent.ConcurrentLinkedQueue
import kotlin.jvm.optionals.getOrNull
import net.minecraft.client.MinecraftClient
@@ -126,6 +127,8 @@ object MC {
}
private set
val currentMoulConfigContext
get() = (screen as? GuiComponentWrapper)?.context
fun openUrl(uri: String) {
Util.getOperatingSystem().open(uri)

View File

@@ -2,6 +2,7 @@ package moe.nea.firmament.util
object TestUtil {
inline fun <T> unlessTesting(block: () -> T): T? = if (isInTest) null else block()
@JvmField
val isInTest =
Thread.currentThread().stackTrace.any {
it.className.startsWith("org.junit.") || it.className.startsWith("io.kotest.")

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig"
>
<Panel background="TRANSPARENT" insets="10">
<Column>
<Meta beforeClose="@beforeClose"/>
<ScrollPanel width="380" height="300">
<Align horizontal="CENTER">
<Array data="@actions">
<!-- evenBackground="#8B8B8B" oddBackground="#C6C6C6" -->
<Panel background="TRANSPARENT" insets="3">
<Panel background="VANILLA" insets="6">
<Column>
<Row>
<Text text="@command" width="280"/>
</Row>
<Row>
<Text text="@formattedCombo" width="250"/>
<Align horizontal="RIGHT">
<Row>
<firm:Button onClick="@edit">
<Text text="Edit"/>
</firm:Button>
<Spacer width="12"/>
<firm:Button onClick="@delete">
<Text text="Delete"/>
</firm:Button>
</Row>
</Align>
</Row>
</Column>
</Panel>
</Panel>
</Array>
</Align>
</ScrollPanel>
<Align horizontal="RIGHT">
<Row>
<firm:Button onClick="@discard">
<Text text="Discard Changes"/>
</firm:Button>
<firm:Button onClick="@saveAndClose">
<Text text="Save &amp; Close"/>
</firm:Button>
<firm:Button onClick="@save">
<Text text="Save"/>
</firm:Button>
<firm:Button onClick="@addCommand">
<Text text="Add Combo Command"/>
</firm:Button>
</Row>
</Align>
</Column>
</Panel>
</Root>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig"
>
<Center>
<Panel background="VANILLA" insets="10">
<Column>
<Row>
<firm:Button onClick="@back">
<Text text="←"/>
</firm:Button>
<Text text="Editing command macro"/>
</Row>
<Row>
<Text text="Command: /"/>
<Align horizontal="RIGHT">
<TextField value="@command" width="200"/>
</Align>
</Row>
<Row>
<Text text="Key Combo:"/>
<Align horizontal="RIGHT">
<firm:Button onClick="@addStep">
<Text text="+"/>
</firm:Button>
</Align>
</Row>
<Array data="@combo">
<Row>
<firm:Fixed width="160">
<Indirect value="@button"/>
</firm:Fixed>
<Align horizontal="RIGHT">
<firm:Button onClick="@delete">
<Text text="Delete"/>
</firm:Button>
</Align>
</Row>
</Array>
</Column>
</Panel>
</Center>
</Root>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Root xmlns="http://notenoughupdates.org/moulconfig"
xmlns:firm="http://firmament.nea.moe/moulconfig">
<Center>
<Tabs>
<Tab>
<Tab.Header>
<Text text="Combo Macros"/>
</Tab.Header>
<Tab.Body>
<Fragment value="firmament:gui/config/macros/combos.xml" bind="@combos"/>
</Tab.Body>
</Tab>
</Tabs>
</Center>
</Root>