From 255666607231f3ff458505c802639f44448d4921 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 14:09:55 +0200 Subject: [PATCH] Migrated Script Editor shortcut keys to proper ScriptEditorKeys handler with new KeyPreset API --- .../client/gui/util/GuiScriptTextArea.java | 364 +++++++++++++----- .../client/key/impl/ScriptEditorKeys.java | 26 ++ 2 files changed, 283 insertions(+), 107 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 38a83c840..d15c2d9a0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -8,12 +8,14 @@ import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.util.script.*; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import noppes.npcs.client.key.impl.ScriptEditorKeys; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import java.util.regex.Matcher; import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; @@ -67,7 +69,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { public List undoList = new ArrayList<>(); public List redoList = new ArrayList<>(); public boolean undoing = false; - + + // ==================== KEYS ==================== + public static final ScriptEditorKeys KEYS = new ScriptEditorKeys(); + // ==================== CONSTRUCTOR ==================== public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { @@ -80,6 +85,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.undoing = true; this.setText(text); this.undoing = false; + initializeKeyBindings(); } // ==================== RENDERING ==================== @@ -420,7 +426,154 @@ public class GuiScriptTextArea extends GuiNpcTextField { scroll.scrollToLine(lineIdx, visible, maxScroll); } - + + // ==================== KEY BINDINGS INITIALIZATION ==================== + + /** + * Initialize key bindings for editor shortcuts using ScriptEditorKeys. + * Centralized active/enabled checking ensures shortcuts only fire when appropriate. + */ + private void initializeKeyBindings() { + // Helper: execute action only if text area is active and enabled + Consumer executeIfActive = (action) -> { + if (active && isEnabled()) { + action.run(); + } + }; + + // CUT: Copy selection to clipboard and delete it + KEYS.CUT.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(() -> { + if (selection.hasSelection()) { + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); + String s = getSelectionBeforeText(); + setText(s + getSelectionAfterText()); + selection.reset(s.length()); + scrollToCursor(); + } + }); + }); + + // COPY: Copy selection to clipboard + KEYS.COPY.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(() -> { + if (selection.hasSelection()) { + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); + } + }); + }); + + // PASTE: Insert clipboard contents at caret + KEYS.PASTE.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(() -> { + addText(NoppesStringUtils.getClipboardContents()); + scrollToCursor(); + }); + }); + + // UNDO: Restore last edit from undo list + KEYS.UNDO.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(() -> { + if (undoList.isEmpty()) + return; + undoing = true; + redoList.add(new UndoData(this.text, selection.getCursorPosition())); + UndoData data = undoList.remove(undoList.size() - 1); + setText(data.text); + selection.reset(data.cursorPosition); + undoing = false; + scrollToCursor(); + }); + }); + + // REDO: Restore last undone edit from redo list + KEYS.REDO.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(() -> { + if (redoList.isEmpty()) + return; + undoing = true; + undoList.add(new UndoData(this.text, selection.getCursorPosition())); + UndoData data = redoList.remove(redoList.size() - 1); + setText(data.text); + selection.reset(data.cursorPosition); + undoing = false; + scrollToCursor(); + }); + }); + + // FORMAT: Format/indent code + KEYS.FORMAT.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(this::formatText); + }); + + // TOGGLE_COMMENT: Toggle comment for selection or current line + KEYS.TOGGLE_COMMENT.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(() -> { + if (selection.hasSelection()) { + toggleCommentSelection(); + } else { + toggleCommentLineAtCursor(); + } + }); + }); + + // DUPLICATE: Duplicate selection or current line + KEYS.DUPLICATE.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(() -> { + if (selection.hasSelection()) { + // Multi-line selection duplication + LineData firstLine = null, lastLine = null; + for (LineData line : container.lines) { + if (line.end > selection.getStartSelection() && line.start < selection.getEndSelection()) { + if (firstLine == null) + firstLine = line; + lastLine = line; + } + } + if (firstLine != null && lastLine != null) { + String selectedText = text.substring(firstLine.start, lastLine.end); + int savedStart = selection.getStartSelection(); + int savedEnd = selection.getEndSelection(); + int insertAt = lastLine.end; + setText(text.substring(0, insertAt) + selectedText + text.substring(insertAt)); + selection.setStartSelection(savedStart); + selection.setEndSelection(savedEnd); + selection.setCursorPositionDirect(savedEnd); + } + } else { + // Duplicate current line + for (LineData line : container.lines) { + if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) { + String lineText = text.substring(line.start, line.end); + String insertText = lineText.endsWith("\n") ? lineText : "\n" + lineText; + int insertionPoint = line.end; + setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint)); + int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0); + selection.reset(Math.max(0, Math.min(newCursor, this.text.length()))); + break; + } + } + } + }); + }); + } + // ==================== KEYBOARD INPUT HANDLING ==================== /** @@ -429,20 +582,18 @@ public class GuiScriptTextArea extends GuiNpcTextField { */ @Override public boolean textboxKeyTyped(char c, int i) { - if (!active) - return false; + if (!active) return false; if (this.isKeyComboCtrlA(i)) { selection.selectAll(text.length()); return true; } - if (!isEnabled()) - return false; + if (!isEnabled()) return false; if (handleNavigationKeys(i)) return true; + if (handleInsertionKeys(i)) return true; if (handleDeletionKeys(i)) return true; - if (handleShortcutKeys(i)) return true; if (handleCharacterInput(c)) return true; return true; @@ -510,6 +661,103 @@ public class GuiScriptTextArea extends GuiNpcTextField { return false; // not a navigation key } + /** + * Handles insertion-related keys such as Tab indentation and Enter behavior. + */ + private boolean handleInsertionKeys(int i) { + // TAB: indent or unindent depending on Shift + if (i == Keyboard.KEY_TAB) { + boolean shift = isShiftKeyDown(); + if (shift) { + handleShiftTab(); + } else { + handleTab(); + } + scrollToCursor(); + return true; + } + + // RETURN/ENTER: special handling when preceding char is an opening brace '{' + if (i == Keyboard.KEY_RETURN) { + int cursorPos = selection.getCursorPosition(); + int prevNonWs = cursorPos - 1; + while (prevNonWs >= 0 && prevNonWs < (text != null ? text.length() : 0) && Character.isWhitespace( + text.charAt(prevNonWs))) { + prevNonWs--; + } + + if (prevNonWs >= 0 && cursorPos <= (text != null ? text.length() : 0) && text.charAt(prevNonWs) == '{') { + String indent = ""; + for (LineData ld : this.container.lines) { + if (prevNonWs >= ld.start && prevNonWs < ld.end) { + indent = ld.text.substring(0, IndentHelper.getLineIndent(ld.text)); + break; + } + } + if (indent == null) + indent = ""; + String childIndent = indent + " "; + String before = getSelectionBeforeText(); + String after = getSelectionAfterText(); + + int firstNewline = after.indexOf('\n'); + String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline); + if (leadingSegment.trim().length() > 0) { + addText("\n" + childIndent); + scrollToCursor(); + return true; + } + + boolean hasMatchingCloseSameIndent = false; + try { + int openLineIdx = -1; + int bracePos = prevNonWs; + for (int li = 0; li < this.container.lines.size(); li++) { + LineData ld = this.container.lines.get(li); + if (bracePos >= ld.start && bracePos < ld.end) { + openLineIdx = li; + break; + } + } + + if (openLineIdx >= 0) { + List spans = BracketMatcher.computeBraceSpans(text, this.container.lines); + for (int[] span : spans) { + int spanOpen = span[1]; + int spanClose = span[2]; + if (spanOpen == openLineIdx) { + int closeIndent = IndentHelper.getLineIndent(this.container.lines.get(spanClose).text); + if (closeIndent == indent.length()) { + hasMatchingCloseSameIndent = true; + } + break; + } + } + } + } catch (Exception ex) { + hasMatchingCloseSameIndent = false; + } + + if (hasMatchingCloseSameIndent) { + addText("\n" + childIndent); + scrollToCursor(); + } else { + String insert = "\n" + childIndent + "\n" + indent + "}"; + setText(before + insert + after); + int newCursor = before.length() + 1 + childIndent.length(); + selection.reset(newCursor); + scrollToCursor(); + } + } else { + addText(Character.toString('\n') + getAutoIndentForEnter()); + scrollToCursor(); + } + return true; + } + + return false; + } + /** * Handles deletion keys: Delete, Backspace, and Ctrl+Backspace. * Includes smart backspace behavior for indentation-aware line merging, @@ -770,112 +1018,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { return true; } - // TAB: indent or unindent depending on Shift - if (i == Keyboard.KEY_TAB) { - boolean shift = isShiftKeyDown(); - if (shift) { - handleShiftTab(); - } else { - handleTab(); - } - scrollToCursor(); - return true; - } - // CTRL+F: format the text according to IndentHelper rules if (i == Keyboard.KEY_F && isCtrlKeyDown()) { formatText(); return true; } - // RETURN/ENTER: special handling when preceding char is an opening brace '{' - if (i == Keyboard.KEY_RETURN) { - // If there is an opening brace `{` before the caret (ignoring any - // intervening whitespace), treat Enter as creating an indented child - // block. This allows the user to type `{ ` and still - // receive the scaffold. - int cursorPos = selection.getCursorPosition(); - int prevNonWs = cursorPos - 1; - while (prevNonWs >= 0 && prevNonWs < (text != null ? text.length() : 0) && Character.isWhitespace(text.charAt(prevNonWs))) { - prevNonWs--; - } - - if (prevNonWs >= 0 && cursorPos <= (text != null ? text.length() : 0) && text.charAt(prevNonWs) == '{') { - // Compute indent using the line that contains the brace so trailing - // spaces after the brace do not affect the computed base indent. - String indent = ""; - for (LineData ld : this.container.lines) { - if (prevNonWs >= ld.start && prevNonWs < ld.end) { - indent = ld.text.substring(0, IndentHelper.getLineIndent(ld.text)); - break; - } - } - if (indent == null) indent = ""; - String childIndent = indent + " "; - String before = getSelectionBeforeText(); - String after = getSelectionAfterText(); - - // If there's non-empty code after the brace on the same line, - // it should be moved into the newly created inner line. - int firstNewline = after.indexOf('\n'); - String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline); - if (leadingSegment.trim().length() > 0) { - addText("\n" + childIndent); - scrollToCursor(); - return true; - } - - // Determine whether this opening brace already has a matching closing - // brace at the same scope (and indent). Use the brace position found - // above (prevNonWs) when computing spans. - boolean hasMatchingCloseSameIndent = false; - try { - int openLineIdx = -1; - int bracePos = prevNonWs; - for (int li = 0; li < this.container.lines.size(); li++) { - LineData ld = this.container.lines.get(li); - if (bracePos >= ld.start && bracePos < ld.end) { - openLineIdx = li; - break; - } - } - - if (openLineIdx >= 0) { - List spans = BracketMatcher.computeBraceSpans(text, this.container.lines); - for (int[] span : spans) { - int spanOpen = span[1]; - int spanClose = span[2]; - if (spanOpen == openLineIdx) { - int closeIndent = IndentHelper.getLineIndent(this.container.lines.get(spanClose).text); - if (closeIndent == indent.length()) { - hasMatchingCloseSameIndent = true; - } - break; - } - } - } - } catch (Exception ex) { - hasMatchingCloseSameIndent = false; - } - - if (hasMatchingCloseSameIndent) { - addText("\n" + childIndent); - scrollToCursor(); - } else { - String insert = "\n" + childIndent + "\n" + indent + "}"; - setText(before + insert + after); - int newCursor = before.length() + 1 + childIndent.length(); - selection.reset(newCursor); - scrollToCursor(); - } - } else { - // Normal enter: insert newline + auto indent for caret position - addText(Character.toString('\n') + getAutoIndentForEnter()); - scrollToCursor(); - } - return true; - } - // CTRL+/ : toggle comment for selection or current line if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) { if (selection.hasSelection()) { @@ -1261,7 +1409,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } + // Called from GuiScreen.updateScreen() public void updateCursorCounter() { + KEYS.tick(); ++this.cursorCounter; } diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java new file mode 100644 index 000000000..48e6df323 --- /dev/null +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -0,0 +1,26 @@ +package noppes.npcs.client.key.impl; + +import noppes.npcs.client.key.KeyPreset; +import noppes.npcs.client.key.KeyPresetManager; +import org.lwjgl.input.Keyboard; + +public class ScriptEditorKeys extends KeyPresetManager { + + //////////////////// + // Editor shortcuts (mapped from GuiScriptTextArea.handleShortcutKeys) + public final KeyPreset CUT = add("Cut").setDefaultState(Keyboard.KEY_X, true, false, false).setDescription("Cut selection or current line"); + public final KeyPreset COPY = add("Copy").setDefaultState(Keyboard.KEY_C, true, false, false).setDescription("Copy selection"); + public final KeyPreset PASTE = add("Paste").setDefaultState(Keyboard.KEY_V, true, false, false).setDescription("Paste clipboard at caret"); + public final KeyPreset DUPLICATE = add("Duplicate").setDefaultState(Keyboard.KEY_D, true, false, false).setDescription("Duplicate selection or current line"); + + + public final KeyPreset UNDO = add("Undo").setDefaultState(Keyboard.KEY_Z, true, false, false).setDescription("Undo last edit"); + public final KeyPreset REDO = add("Redo").setDefaultState(Keyboard.KEY_Y, true, false, false).setDescription("Redo last undone edit"); + public final KeyPreset FORMAT = add("Format").setDefaultState(Keyboard.KEY_F, true, false, false).setDescription("Format code / indent"); + public final KeyPreset TOGGLE_COMMENT = add("Toggle Comment").setDefaultState(Keyboard.KEY_SLASH, true, false, false).setDescription("Toggle comment for selection or line"); + + public ScriptEditorKeys() { + super("script_editor"); + load(); + } +}