diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/decorative/HoloSignBlockEntity.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/decorative/HoloSignBlockEntity.kt index 9f49627bb..95f9429a3 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/decorative/HoloSignBlockEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/decorative/HoloSignBlockEntity.kt @@ -30,7 +30,7 @@ class HoloSignBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryB override val redstoneControl = SynchronizedRedstoneControl(syncher) { _, _ -> setChanged() } var signText by syncher.string("", setter = { access, value -> - setChanged() + markDirtyFast() access.accept(value) }).delegate @@ -96,24 +96,25 @@ class HoloSignBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryB override fun loadAdditional(nbt: CompoundTag, registry: HolderLookup.Provider) { super.loadAdditional(nbt, registry) - - if (!isLocked) { - signText = truncate(signText) - } + signText = truncate(signText, isLocked) } companion object { const val DEFAULT_MAX_NEWLINES = 10 const val DEFAULT_MAX_LINE_LENGTH = 60 + const val CREATIVE_MAX_NEWLINES = 60 + const val CREATIVE_MAX_LINE_LENGTH = 240 private val NEWLINES = Regex("\r?\n") - fun truncate(input: String): String { + fun truncate(input: String, isLocked: Boolean): String { + val maxLines = if (isLocked) CREATIVE_MAX_NEWLINES else DEFAULT_MAX_NEWLINES + val maxLength = if (isLocked) CREATIVE_MAX_LINE_LENGTH else DEFAULT_MAX_LINE_LENGTH val lines = input.split(NEWLINES) - val result = ArrayList(lines.size.coerceAtMost(DEFAULT_MAX_NEWLINES)) + val result = ArrayList(lines.size.coerceAtMost(maxLines)) - for (i in 0 until lines.size.coerceAtMost(DEFAULT_MAX_NEWLINES)) { - if (lines[i].length > DEFAULT_MAX_LINE_LENGTH) { - result.add(lines[i].substring(0, DEFAULT_MAX_LINE_LENGTH)) + for (i in 0 until lines.size.coerceAtMost(maxLines)) { + if (lines[i].length > maxLength) { + result.add(lines[i].substring(0, maxLength)) } else { result.add(lines[i]) } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/decorative/HoloSignScreen.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/decorative/HoloSignScreen.kt index bce51cba7..95d1b597c 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/decorative/HoloSignScreen.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/decorative/HoloSignScreen.kt @@ -10,6 +10,7 @@ import ru.dbotthepony.mc.otm.client.screen.panels.button.makeDeviceControls import ru.dbotthepony.mc.otm.client.screen.panels.input.NetworkedStringInputPanel import ru.dbotthepony.mc.otm.core.TranslatableComponent import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.mc.otm.block.entity.decorative.HoloSignBlockEntity import ru.dbotthepony.mc.otm.client.render.Widgets18 import ru.dbotthepony.mc.otm.client.screen.panels.button.BooleanButtonPanel import ru.dbotthepony.mc.otm.client.screen.panels.button.ButtonPanel @@ -17,7 +18,7 @@ import ru.dbotthepony.mc.otm.menu.decorative.HoloSignMenu class HoloSignScreen(menu: HoloSignMenu, inventory: Inventory, title: Component) : MatteryScreen(menu, title) { override fun makeMainFrame(): FramePanel> { - val frame = FramePanel(this, null, 0f, 0f, 200f, 200f, getTitle()) + val frame = FramePanel(this, null, 0f, 0f, minecraft!!.window.guiScaledWidth * 0.8f, minecraft!!.window.guiScaledHeight * 0.8f, getTitle()) frame.makeCloseButton() frame.onClose { onClose() } @@ -25,9 +26,19 @@ class HoloSignScreen(menu: HoloSignMenu, inventory: Inventory, title: Component) tooltips.add(TranslatableComponent("otm.gui.lock_holo_screen.tip")) } - val input = NetworkedStringInputPanel(this, frame, backend = menu.text) - input.dock = Dock.FILL - input.isMultiLine = true + object : NetworkedStringInputPanel(this@HoloSignScreen, frame, backend = menu.text) { + init { + dock = Dock.FILL + isMultiLine = true + } + + override var maxLineLength: Int + get() = if (menu.locked.value) HoloSignBlockEntity.CREATIVE_MAX_LINE_LENGTH else HoloSignBlockEntity.DEFAULT_MAX_LINE_LENGTH + set(value) {} + override var maxLines: Int + get() = if (menu.locked.value) HoloSignBlockEntity.CREATIVE_MAX_NEWLINES else HoloSignBlockEntity.DEFAULT_MAX_NEWLINES + set(value) {} + } val controls = makeDeviceControls(this, frame, redstoneConfig = menu.redstone) diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/ColorPicker.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/ColorPicker.kt index c8c81f08b..c12c706f1 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/ColorPicker.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/ColorPicker.kt @@ -616,7 +616,7 @@ open class ColorPickerPanel( } override fun acceptsCharacter(codepoint: Char, mods: Int, index: Int): Boolean { - return RGBAColor.isHexCharacter(codepoint) + return super.acceptsCharacter(codepoint, mods, index) && RGBAColor.isHexCharacter(codepoint) } } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/input/TextInputPanel.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/input/TextInputPanel.kt index f8876b02a..7caf0e526 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/input/TextInputPanel.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/input/TextInputPanel.kt @@ -33,6 +33,7 @@ import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.mc.otm.client.render.vertex import ru.dbotthepony.mc.otm.milliTime import java.util.function.Predicate +import kotlin.math.abs import kotlin.math.roundToInt open class TextInputPanel( @@ -87,7 +88,7 @@ open class TextInputPanel( private val lines = ArrayList(this@TextInputPanel.lines) // ultra fast copy private val cursorLine = this@TextInputPanel.cursorLine private val cursorCharacter = this@TextInputPanel.cursorRow - private val selections = Int2ObjectAVLTreeMap(this@TextInputPanel.selections) + private val selections = this@TextInputPanel.selections.clone() private val multiLine = this@TextInputPanel.isMultiLine fun apply() { @@ -155,7 +156,19 @@ open class TextInputPanel( open var isActive = true override val cursorType: CursorType - get() = if (isActive) CursorType.BEAM else CursorType.NOT_ALLOWED + get() = CursorType.BEAM + + open var maxLineLength = Int.MAX_VALUE + set(value) { + require(value in 1 .. Int.MAX_VALUE) { "Max line length value is out of bounds: $value" } + field = value + } + + open var maxLines = Int.MAX_VALUE + set(value) { + require(value in 1 .. Int.MAX_VALUE) { "Max lines value is out of bounds: $value" } + field = value + } init { scissor = true @@ -463,8 +476,6 @@ open class TextInputPanel( lines.clear() redo.clear() undo.clear() - cursorLine = 0 - cursorRow = 0 textCache = null if (isMultiLine) { @@ -472,6 +483,9 @@ open class TextInputPanel( } else { lines.add(value.replace(NEWLINES, "")) } + + cursorLine = minOf(cursorLine, lines.size - 1) + cursorRow = minOf(this[cursorLine]?.length ?: 0, cursorRow) } data class CursorAdvanceResult( @@ -675,6 +689,12 @@ open class TextInputPanel( if (key == InputConstants.KEY_RETURN) { if (isMultiLine) { + if (lines.size >= maxLines) { + playGuiClickSound() + rejectCharacterTimer = milliTime + SHAKE_MILLIS + return true + } + if (!minecraft.window.isShiftDown && !minecraft.window.isCtrlDown) wipeSelection() @@ -729,26 +749,34 @@ open class TextInputPanel( } if (key == InputConstants.KEY_LEFT) { - if (!minecraft.window.isShiftDown) { - advanceCursorLeft(minecraft.window.isCtrlDown) - selections.clear() - } else { + if (minecraft.window.isShiftDown) { if (this[cursorLine] == null) return true simulateSelectLeft(minecraft.window.isCtrlDown) + } else if (selections.isNotEmpty()) { + val (index, selection) = selections.firstEntry() + cursorLine = index + cursorRow = selection.cursor + selections.clear() + } else { + advanceCursorLeft(minecraft.window.isCtrlDown) } return true } else if (key == InputConstants.KEY_RIGHT) { - if (!minecraft.window.isShiftDown) { - advanceCursorRight(minecraft.window.isCtrlDown) - selections.clear() - } else { + if (minecraft.window.isShiftDown) { if (this[cursorLine] == null) return true simulateSelectRight(minecraft.window.isCtrlDown) + } else if (selections.isNotEmpty()) { + val (index, selection) = selections.lastEntry() + cursorLine = index + cursorRow = selection.cursor + selection.shift + selections.clear() + } else { + advanceCursorRight(minecraft.window.isCtrlDown) } return true @@ -911,12 +939,31 @@ open class TextInputPanel( wipeSelection() pushbackSnapshot() + var shouldPlayClick = false + + fun trimLine(input: String): String { + if (input.length < maxLineLength) { + return input + } else { + shouldPlayClick = true + return input.substring(0, maxLineLength) + } + } + if (isMultiLine) { + if (cursorLine >= maxLines) { + playGuiClickSound() + rejectCharacterTimer = milliTime + SHAKE_MILLIS + return true + } + var index = cursorRow + (0 until cursorLine).iterator().map { this[it]?.length ?: 0 }.reduce(0, Int::plus) val insert = minecraft.keyboardHandler.clipboard.replace("\t", " ").filter { acceptsCharacter(it, 0, index++) }.split(NEWLINES).toMutableList() val actualLastSize = insert.lastOrNull()?.length ?: 0 val line = this[cursorLine] + insert[0] = trimLine(insert[0]) + if (line == null) { insertLine(cursorLine, insert[0]) cursorRow = insert[0].length @@ -925,9 +972,9 @@ open class TextInputPanel( cursorRow = insert[0].length } else { if (insert.size == 1) { - this[cursorLine] = line.substring(0, cursorRow.coerceAtMost(line.length - 1)) + insert[0] + line.substring(cursorRow.coerceAtMost(line.length)) + this[cursorLine] = trimLine(line.substring(0, cursorRow.coerceAtMost(line.length)) + insert[0] + line.substring(cursorRow.coerceAtMost(line.length))) } else { - this[cursorLine] = line.substring(0, cursorRow.coerceAtMost(line.length - 1)) + insert[0] + this[cursorLine] = trimLine(line.substring(0, cursorRow.coerceAtMost(line.length)) + insert[0]) insert[insert.size - 1] += line.substring(cursorRow.coerceAtMost(line.length)) } @@ -935,21 +982,31 @@ open class TextInputPanel( } for (i in 1 until insert.size - 1) { + if (cursorLine + 1 >= maxLines) { + shouldPlayClick = true + break + } + + insert[i] = trimLine(insert[i]) insertLine(++cursorLine, insert[i]) cursorRow = insert[i].length } if (insert.size >= 2) { - val line2 = this[++cursorLine] - val last = insert.last() + if (cursorLine + 1 < maxLines) { + val line2 = this[++cursorLine] + val last = insert.last() - if (line2 == null) { - insertLine(cursorLine, last) + if (line2 == null) { + insertLine(cursorLine, trimLine(last)) + } else { + this[cursorLine] = trimLine(last + line2) + } + + cursorRow = actualLastSize } else { - this[cursorLine] = last + line2 + shouldPlayClick = true } - - cursorRow = actualLastSize } } else { var index = cursorRow + (0 until cursorLine).iterator().map { this[it]?.length ?: 0 }.reduce(0, Int::plus) @@ -957,17 +1014,26 @@ open class TextInputPanel( val line = this[cursorLine] if (line == null) { - insertLine(cursorLine, insert) - cursorRow = insert.length + val trim = trimLine(insert) + insertLine(cursorLine, trim) + cursorRow = trim.length } else if (line.isEmpty()) { - this[cursorLine] = insert - cursorRow = insert.length + val trim = trimLine(insert) + this[cursorLine] = trim + cursorRow = trim.length } else { - this[cursorLine] = line.substring(0, cursorRow.coerceAtMost(line.length - 1)) + insert + line.substring(cursorRow.coerceAtMost(line.length)) - cursorRow += insert.length + val potential = line.substring(0, cursorRow.coerceAtMost(line.length - 1)) + insert + line.substring(cursorRow.coerceAtMost(line.length)) + val final = trimLine(potential) + this[cursorLine] = final + cursorRow += maxOf(0, insert.length - (potential.length - final.length)) } } + if (shouldPlayClick) { + playGuiClickSound() + rejectCharacterTimer = milliTime + SHAKE_MILLIS + } + pushbackSnapshot() triggerChangeCallback() return true @@ -1078,6 +1144,11 @@ open class TextInputPanel( if (!isMultiLine) cursorLine = 0 + else if (cursorLine >= maxLines) { + playGuiClickSound() + rejectCharacterTimer = milliTime + SHAKE_MILLIS + return true + } var line = this[cursorLine] @@ -1091,7 +1162,7 @@ open class TextInputPanel( for (i in 0 until cursorLine) index += this[i]?.length ?: 0 index += cursorRow - if (!acceptsCharacter(codepoint, mods, index)) { + if (cursorRow >= maxLineLength || !acceptsCharacter(codepoint, mods, index)) { playGuiClickSound() rejectCharacterTimer = milliTime + SHAKE_MILLIS return true @@ -1195,6 +1266,22 @@ open class TextInputPanel( ) } + if (maxLines < 100000) { + val p = maxLines * (font.lineHeight + rowSpacing) + dockPaddingTop + graphics.drawLine(0f, p, 1000000f, p, 2f, color = RGBAColor.GRAY) + } + + // TODO: Minecraft font is not mono spaced, so we can't properly render per-line character bounds + if (selectedLine != null && abs(selectedLine.length - maxLineLength) < 200) { + val lineWidth = if (selectedLine.length >= maxLineLength) { + font.width(selectedLine.substring(0, maxLineLength)).toFloat() + } else { + font.width(selectedLine + "a".repeat(maxLineLength - selectedLine.length)).toFloat() + } + + graphics.drawLine(lineWidth + 4f, 0f, lineWidth + 4f, 1000000f, 2f, color = RGBAColor.GRAY) + } + for (i in scrollLines until lines.size) { val line = lines[i] val selection = selections[i] @@ -1303,9 +1390,6 @@ open class TextInputPanel( protected set override fun mouseClickedInner(x: Double, y: Double, button: Int): Boolean { - if (!isActive) - return true - if (isEverFocused()) selections.clear() @@ -1321,7 +1405,7 @@ open class TextInputPanel( isSelecting = true tryToGrabMouseInput() - } else if (button == InputConstants.MOUSE_BUTTON_RIGHT && !isMultiLine) { + } else if (button == InputConstants.MOUSE_BUTTON_RIGHT && !isMultiLine && isActive) { text = "" } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/decorative/HoloSignMenu.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/decorative/HoloSignMenu.kt index 2dd7ffa7c..d62e67460 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/decorative/HoloSignMenu.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/decorative/HoloSignMenu.kt @@ -39,7 +39,7 @@ class HoloSignMenu( textAutoScale.filter { it.isCreative || !locked.value } if (tile != null) { - text.withConsumer { if (tile.isLocked) tile.signText = it else tile.signText = HoloSignBlockEntity.truncate(it) }.withSupplier(tile::signText) + text.withConsumer { tile.signText = HoloSignBlockEntity.truncate(it, tile.isLocked) }.withSupplier(tile::signText) textRed.withConsumer { tile.textRed = it.coerceIn(0f, 1f) }.withSupplier(tile::textRed) textGreen.withConsumer { tile.textGreen = it.coerceIn(0f, 1f) }.withSupplier(tile::textGreen) textBlue.withConsumer { tile.textBlue = it.coerceIn(0f, 1f) }.withSupplier(tile::textBlue)