mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 06:07:27 +00:00
CRAZY GuiScriptTextArea MODULATION (W OPUS 4.5)
This commit is contained in:
1 parent
6373839870
commit
79edd08ff4
8 files changed
+1571
-488
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,252 @@
|
||||
package noppes.npcs.client.gui.util.script;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Handles bracket matching, brace span computation, and indent guide generation.
|
||||
*/
|
||||
public class BracketMatcher {
|
||||
|
||||
/**
|
||||
* Find matching bracket at cursor position.
|
||||
* @return int[2] with {startPos, endPos} or null if no bracket found
|
||||
*/
|
||||
public static int[] findBracketSpanAt(String text, int pos) {
|
||||
if (text == null || text.isEmpty()) return null;
|
||||
|
||||
// Try at current position
|
||||
if (pos >= 0 && pos < text.length()) {
|
||||
char c = text.charAt(pos);
|
||||
int found = 0;
|
||||
if (c == '{') found = findClosingBracket(text.substring(pos), '{', '}');
|
||||
else if (c == '[') found = findClosingBracket(text.substring(pos), '[', ']');
|
||||
else if (c == '(') found = findClosingBracket(text.substring(pos), '(', ')');
|
||||
else if (c == '}') found = findOpeningBracket(text.substring(0, pos + 1), '{', '}');
|
||||
else if (c == ']') found = findOpeningBracket(text.substring(0, pos + 1), '[', ']');
|
||||
else if (c == ')') found = findOpeningBracket(text.substring(0, pos + 1), '(', ')');
|
||||
if (found != 0) return new int[]{pos, pos + found};
|
||||
}
|
||||
|
||||
// Scan backwards, skipping whitespace on same line
|
||||
if (pos > 0) {
|
||||
int scan = pos - 1;
|
||||
while (scan >= 0) {
|
||||
char sc = text.charAt(scan);
|
||||
if (sc == ' ' || sc == '\t') { scan--; continue; }
|
||||
if (sc == '\n') break; // Don't cross line boundary
|
||||
|
||||
int found2 = 0;
|
||||
if (sc == '{') found2 = findClosingBracket(text.substring(scan), '{', '}');
|
||||
else if (sc == '[') found2 = findClosingBracket(text.substring(scan), '[', ']');
|
||||
else if (sc == '(') found2 = findClosingBracket(text.substring(scan), '(', ')');
|
||||
else if (sc == '}') found2 = findOpeningBracket(text.substring(0, scan + 1), '{', '}');
|
||||
else if (sc == ']') found2 = findOpeningBracket(text.substring(0, scan + 1), '[', ']');
|
||||
else if (sc == ')') found2 = findOpeningBracket(text.substring(0, scan + 1), '(', ')');
|
||||
|
||||
if (found2 != 0) return new int[]{scan, scan + found2};
|
||||
break; // Not a bracket; stop
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find closing bracket, returns offset from start or 0 if not found
|
||||
*/
|
||||
public static int findClosingBracket(String str, char open, char close) {
|
||||
int depth = 0;
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
char c = str.charAt(i);
|
||||
if (c == open) depth++;
|
||||
else if (c == close) {
|
||||
depth--;
|
||||
if (depth == 0) return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find opening bracket, returns negative offset from end or 0 if not found
|
||||
*/
|
||||
public static int findOpeningBracket(String str, char open, char close) {
|
||||
int depth = 0;
|
||||
for (int i = str.length() - 1; i >= 0; i--) {
|
||||
char c = str.charAt(i);
|
||||
if (c == close) depth++;
|
||||
else if (c == open) {
|
||||
depth--;
|
||||
if (depth == 0) return i - str.length() + 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute brace spans for indent guides.
|
||||
* Returns list of int[4]: {originalDepth, openLineIdx, closeLineIdx, adjustedDepth}
|
||||
*/
|
||||
public static List<int[]> computeBraceSpans(String text, List<LineData> lines) {
|
||||
List<int[]> spans = new ArrayList<>();
|
||||
if (text == null || text.isEmpty() || lines == null || lines.isEmpty())
|
||||
return spans;
|
||||
|
||||
int lineIdx = 0;
|
||||
int lineEnd = lines.get(0).end;
|
||||
List<Integer> openStack = new ArrayList<>();
|
||||
|
||||
boolean inLineComment = false;
|
||||
boolean inBlockComment = false;
|
||||
boolean inString = false;
|
||||
boolean escape = false;
|
||||
char stringDelimiter = 0;
|
||||
|
||||
for (int pos = 0; pos < text.length(); pos++) {
|
||||
// Advance line index
|
||||
while (lineIdx < lines.size() - 1 && pos >= lineEnd) {
|
||||
lineIdx++;
|
||||
lineEnd = lines.get(lineIdx).end;
|
||||
}
|
||||
|
||||
char c = text.charAt(pos);
|
||||
char next = pos + 1 < text.length() ? text.charAt(pos + 1) : 0;
|
||||
|
||||
// String handling
|
||||
if (inString) {
|
||||
if (escape) { escape = false; }
|
||||
else if (c == '\\') { escape = true; }
|
||||
else if (c == stringDelimiter) { inString = false; }
|
||||
else if (c == '\n') { inString = false; } // Unterminated string
|
||||
continue;
|
||||
}
|
||||
|
||||
// Block comment handling
|
||||
if (inBlockComment) {
|
||||
if (c == '*' && next == '/') { inBlockComment = false; pos++; }
|
||||
continue;
|
||||
}
|
||||
|
||||
// Line comment handling
|
||||
if (inLineComment) {
|
||||
if (c == '\n') { inLineComment = false; }
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start of comments
|
||||
if (c == '/' && next == '/') { inLineComment = true; pos++; continue; }
|
||||
if (c == '/' && next == '*') { inBlockComment = true; pos++; continue; }
|
||||
|
||||
// Start of string
|
||||
if (c == '"' || c == '\'') {
|
||||
inString = true;
|
||||
stringDelimiter = c;
|
||||
escape = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Brace matching
|
||||
if (c == '{') {
|
||||
openStack.add(lineIdx);
|
||||
} else if (c == '}') {
|
||||
if (!openStack.isEmpty()) {
|
||||
int openLine = openStack.remove(openStack.size() - 1);
|
||||
int spanDepth = openStack.size() + 1;
|
||||
spans.add(new int[]{spanDepth, openLine, lineIdx});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize depths for unmatched braces
|
||||
if (!openStack.isEmpty()) {
|
||||
int baseline = openStack.size();
|
||||
List<int[]> adjusted = new ArrayList<>(spans.size());
|
||||
for (int[] span : spans) {
|
||||
int adjustedDepth = Math.max(1, span[0] - baseline);
|
||||
adjusted.add(new int[]{span[0], span[1], span[2], adjustedDepth});
|
||||
}
|
||||
return adjusted;
|
||||
} else {
|
||||
for (int i = 0; i < spans.size(); i++) {
|
||||
int[] span = spans.get(i);
|
||||
spans.set(i, new int[]{span[0], span[1], span[2], span[0]});
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find which brace span should be highlighted based on bracket at position
|
||||
* @return int[2] {openLine, closeLine} or null
|
||||
*/
|
||||
public static int[] findHighlightedSpan(int bracketPos, List<LineData> lines, List<int[]> spans) {
|
||||
if (bracketPos < 0 || lines == null || spans == null) return null;
|
||||
|
||||
// Find line containing bracket
|
||||
int bracketLineIdx = -1;
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
LineData ld = lines.get(i);
|
||||
if (bracketPos >= ld.start && bracketPos < ld.end) {
|
||||
bracketLineIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bracketLineIdx >= 0) {
|
||||
for (int[] span : spans) {
|
||||
int openLine = span[1];
|
||||
int closeLine = span[2];
|
||||
if (bracketLineIdx >= openLine && bracketLineIdx <= closeLine) {
|
||||
return new int[]{openLine, closeLine};
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a brace at given position has a matching close with same indent
|
||||
*/
|
||||
public static boolean hasMatchingCloseWithSameIndent(String text, int bracePos, List<LineData> lines, int currentIndent) {
|
||||
if (text == null || lines == null || bracePos < 0) return false;
|
||||
|
||||
// Find line containing the brace
|
||||
int openLineIdx = -1;
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
LineData ld = lines.get(i);
|
||||
if (bracePos >= ld.start && bracePos < ld.end) {
|
||||
openLineIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (openLineIdx < 0) return false;
|
||||
|
||||
List<int[]> spans = computeBraceSpans(text, lines);
|
||||
for (int[] span : spans) {
|
||||
if (span[1] == openLineIdx) {
|
||||
int closeLineIdx = span[2];
|
||||
if (closeLineIdx < lines.size()) {
|
||||
String closeLine = lines.get(closeLineIdx).text;
|
||||
int closeIndent = getLineIndent(closeLine);
|
||||
return closeIndent == currentIndent;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get leading whitespace count of a line
|
||||
*/
|
||||
private static int getLineIndent(String line) {
|
||||
int indent = 0;
|
||||
for (int i = 0; i < line.length(); i++) {
|
||||
char c = line.charAt(i);
|
||||
if (c == ' ' || c == '\t') indent++;
|
||||
else break;
|
||||
}
|
||||
return indent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package noppes.npcs.client.gui.util.script;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Handles line comment toggling for the script editor.
|
||||
*/
|
||||
public class CommentHandler {
|
||||
|
||||
/**
|
||||
* Result of comment toggle operation
|
||||
*/
|
||||
public static class ToggleResult {
|
||||
public final String text;
|
||||
public final int delta; // Change in length (negative if comment removed)
|
||||
public final int nonWsIndex; // Index of first non-whitespace character
|
||||
|
||||
public ToggleResult(String text, int delta, int nonWsIndex) {
|
||||
this.text = text;
|
||||
this.delta = delta;
|
||||
this.nonWsIndex = nonWsIndex;
|
||||
}
|
||||
|
||||
public boolean wasCommented() {
|
||||
return delta < 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle line comment on a single line
|
||||
*/
|
||||
public static ToggleResult toggleLine(String lineText) {
|
||||
// Find first non-whitespace
|
||||
int nonWs = 0;
|
||||
while (nonWs < lineText.length() && Character.isWhitespace(lineText.charAt(nonWs))) {
|
||||
nonWs++;
|
||||
}
|
||||
|
||||
boolean hasContent = nonWs < lineText.length();
|
||||
boolean hasComment = hasContent && lineText.startsWith("//", nonWs);
|
||||
String newText;
|
||||
|
||||
if (hasComment) {
|
||||
// Remove comment
|
||||
int cut = Math.min(nonWs + 2, lineText.length());
|
||||
newText = lineText.substring(0, nonWs) + lineText.substring(cut);
|
||||
} else if (hasContent) {
|
||||
// Add comment
|
||||
newText = lineText.substring(0, nonWs) + "//" + lineText.substring(nonWs);
|
||||
} else {
|
||||
// Empty/whitespace line - no change
|
||||
newText = lineText;
|
||||
}
|
||||
|
||||
return new ToggleResult(newText, newText.length() - lineText.length(), nonWs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of toggling comments on a selection
|
||||
*/
|
||||
public static class SelectionToggleResult {
|
||||
public final String newText;
|
||||
public final int newStartSelection;
|
||||
public final int newEndSelection;
|
||||
|
||||
public SelectionToggleResult(String text, int start, int end) {
|
||||
this.newText = text;
|
||||
this.newStartSelection = start;
|
||||
this.newEndSelection = end;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle comments on all lines in a selection
|
||||
*/
|
||||
public static SelectionToggleResult toggleCommentSelection(
|
||||
String text,
|
||||
List<LineData> lines,
|
||||
int startSelection,
|
||||
int endSelection) {
|
||||
|
||||
StringBuilder newText = new StringBuilder(text.length() + 100);
|
||||
int prevEnd = 0;
|
||||
int newStart = -1;
|
||||
int newEnd = -1;
|
||||
int newIndex = 0;
|
||||
|
||||
for (LineData line : lines) {
|
||||
int safeLineStart = Math.max(0, Math.min(line.start, text.length()));
|
||||
int safeLineEnd = Math.max(0, Math.min(line.end, text.length()));
|
||||
|
||||
// Handle gap between lines (shouldn't happen normally)
|
||||
if (safeLineStart > prevEnd) {
|
||||
if (newStart == -1 && startSelection >= prevEnd && startSelection <= safeLineStart) {
|
||||
newStart = newIndex + (startSelection - prevEnd);
|
||||
}
|
||||
if (newEnd == -1 && endSelection >= prevEnd && endSelection <= safeLineStart) {
|
||||
newEnd = newIndex + (endSelection - prevEnd);
|
||||
}
|
||||
newText.append(text, prevEnd, safeLineStart);
|
||||
newIndex += safeLineStart - prevEnd;
|
||||
}
|
||||
|
||||
// Check if this line is in selection
|
||||
if (safeLineEnd > startSelection && safeLineStart < endSelection) {
|
||||
String lineText = text.substring(safeLineStart, safeLineEnd);
|
||||
ToggleResult tr = toggleLine(lineText);
|
||||
String newLineText = tr.text;
|
||||
int nonWs = tr.nonWsIndex;
|
||||
int delta = tr.delta;
|
||||
|
||||
// Update start selection position
|
||||
if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) {
|
||||
int offsetInOld = startSelection - safeLineStart;
|
||||
int newOffset = offsetInOld;
|
||||
if (offsetInOld >= nonWs) {
|
||||
newOffset = offsetInOld + delta;
|
||||
}
|
||||
newStart = newIndex + Math.max(0, newOffset);
|
||||
}
|
||||
|
||||
// Update end selection position
|
||||
if (newEnd == -1 && endSelection >= safeLineStart && endSelection <= safeLineEnd) {
|
||||
int offsetInOld = endSelection - safeLineStart;
|
||||
int newOffset = offsetInOld;
|
||||
if (offsetInOld >= nonWs) {
|
||||
newOffset = offsetInOld + delta;
|
||||
}
|
||||
newEnd = newIndex + Math.max(0, newOffset);
|
||||
}
|
||||
|
||||
newText.append(newLineText);
|
||||
newIndex += newLineText.length();
|
||||
} else {
|
||||
// Line not in selection - keep unchanged
|
||||
if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) {
|
||||
newStart = newIndex + (startSelection - safeLineStart);
|
||||
}
|
||||
if (newEnd == -1 && endSelection >= safeLineStart && endSelection <= safeLineEnd) {
|
||||
newEnd = newIndex + (endSelection - safeLineStart);
|
||||
}
|
||||
newText.append(text, safeLineStart, safeLineEnd);
|
||||
newIndex += safeLineEnd - safeLineStart;
|
||||
}
|
||||
prevEnd = safeLineEnd;
|
||||
}
|
||||
|
||||
// Handle remaining text after last line
|
||||
if (prevEnd < text.length()) {
|
||||
if (newStart == -1 && startSelection >= prevEnd && startSelection <= text.length()) {
|
||||
newStart = newIndex + (startSelection - prevEnd);
|
||||
}
|
||||
if (newEnd == -1 && endSelection >= prevEnd && endSelection <= text.length()) {
|
||||
newEnd = newIndex + (endSelection - prevEnd);
|
||||
}
|
||||
newText.append(text, prevEnd, text.length());
|
||||
newIndex += text.length() - prevEnd;
|
||||
}
|
||||
|
||||
// Fallback for positions not found
|
||||
if (newStart == -1) newStart = Math.max(0, Math.min(startSelection, newText.length()));
|
||||
if (newEnd == -1) newEnd = Math.max(0, Math.min(endSelection, newText.length()));
|
||||
|
||||
return new SelectionToggleResult(newText.toString(), newStart, newEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle comment on a single line at cursor position
|
||||
*/
|
||||
public static SingleLineToggleResult toggleCommentAtCursor(
|
||||
String text,
|
||||
List<LineData> lines,
|
||||
int cursorPosition) {
|
||||
|
||||
for (LineData line : lines) {
|
||||
int lineStart = Math.max(0, Math.min(line.start, text.length()));
|
||||
int lineEnd = Math.max(0, Math.min(line.end, text.length()));
|
||||
|
||||
if (cursorPosition >= lineStart && cursorPosition <= lineEnd) {
|
||||
String lineText = text.substring(lineStart, lineEnd);
|
||||
ToggleResult tr = toggleLine(lineText);
|
||||
boolean hadComment = tr.delta < 0;
|
||||
|
||||
String before = text.substring(0, lineStart);
|
||||
String after = lineEnd <= text.length() ? text.substring(lineEnd) : "";
|
||||
String newText = before + tr.text + after;
|
||||
|
||||
// Calculate new cursor position
|
||||
int cursorDelta = hadComment ? -2 : 2;
|
||||
int newCursor = cursorPosition + cursorDelta;
|
||||
if (cursorPosition < lineStart + tr.nonWsIndex + (hadComment ? 2 : 0)) {
|
||||
newCursor = cursorPosition;
|
||||
}
|
||||
newCursor = Math.max(lineStart, newCursor);
|
||||
|
||||
return new SingleLineToggleResult(newText, newCursor);
|
||||
}
|
||||
}
|
||||
|
||||
// No line found at cursor - return unchanged
|
||||
return new SingleLineToggleResult(text, cursorPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of toggling comment on single line
|
||||
*/
|
||||
public static class SingleLineToggleResult {
|
||||
public final String newText;
|
||||
public final int newCursorPosition;
|
||||
|
||||
public SingleLineToggleResult(String text, int cursor) {
|
||||
this.newText = text;
|
||||
this.newCursorPosition = cursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package noppes.npcs.client.gui.util.script;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Handles cursor navigation logic (up, down, left, right, word movement).
|
||||
*/
|
||||
public class CursorNavigation {
|
||||
|
||||
/**
|
||||
* Move cursor up one line, maintaining column position
|
||||
*/
|
||||
public static int cursorUp(int cursorPosition, List<LineData> lines, String text) {
|
||||
if (lines == null || lines.isEmpty()) return 0;
|
||||
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
LineData data = lines.get(i);
|
||||
boolean isLastLine = (i == lines.size() - 1);
|
||||
boolean isOnLine = isLastLine
|
||||
? (cursorPosition >= data.start && cursorPosition <= data.end)
|
||||
: (cursorPosition >= data.start && cursorPosition < data.end);
|
||||
|
||||
if (isOnLine) {
|
||||
if (i == 0) {
|
||||
return 0; // Already on first line
|
||||
}
|
||||
|
||||
int column = Math.min(cursorPosition - data.start, data.text.length());
|
||||
LineData target = lines.get(i - 1);
|
||||
int targetColumn = Math.min(column, target.text.length());
|
||||
int targetIndent = getLineIndent(target.text);
|
||||
int minPos = target.start + Math.min(targetIndent, target.text.length());
|
||||
int targetPos = target.start + targetColumn;
|
||||
return Math.max(minPos, targetPos);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move cursor down one line, maintaining column position
|
||||
*/
|
||||
public static int cursorDown(int cursorPosition, List<LineData> lines, String text) {
|
||||
if (lines == null || lines.isEmpty()) return text != null ? text.length() : 0;
|
||||
if (text != null && cursorPosition == text.length()) return cursorPosition;
|
||||
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
LineData data = lines.get(i);
|
||||
boolean isLastLine = (i == lines.size() - 1);
|
||||
boolean isOnLine = isLastLine
|
||||
? (cursorPosition >= data.start && cursorPosition <= data.end)
|
||||
: (cursorPosition >= data.start && cursorPosition < data.end);
|
||||
|
||||
if (isOnLine) {
|
||||
if (i >= lines.size() - 1) {
|
||||
return cursorPosition; // Already on last line
|
||||
}
|
||||
|
||||
int column = Math.min(cursorPosition - data.start, data.text.length());
|
||||
LineData target = lines.get(i + 1);
|
||||
int targetColumn = Math.min(column, target.text.length());
|
||||
int targetIndent = getLineIndent(target.text);
|
||||
int minPos = target.start + Math.min(targetIndent, target.text.length());
|
||||
int targetPos = target.start + targetColumn;
|
||||
return Math.max(minPos, targetPos);
|
||||
}
|
||||
}
|
||||
return text != null ? text.length() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move cursor left, optionally by word
|
||||
*/
|
||||
public static int cursorLeft(int cursorPosition, String text, Pattern wordPattern, boolean byWord) {
|
||||
if (text == null || cursorPosition <= 0) return 0;
|
||||
|
||||
int moveAmount = 1;
|
||||
if (byWord && wordPattern != null) {
|
||||
Matcher m = wordPattern.matcher(text.substring(0, cursorPosition));
|
||||
while (m.find()) {
|
||||
if (m.start() != m.end()) {
|
||||
moveAmount = cursorPosition - m.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
return Math.max(cursorPosition - moveAmount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move cursor right, optionally by word
|
||||
*/
|
||||
public static int cursorRight(int cursorPosition, String text, Pattern wordPattern, boolean byWord) {
|
||||
if (text == null) return 0;
|
||||
int textLen = text.length();
|
||||
if (cursorPosition >= textLen) return textLen;
|
||||
|
||||
int moveAmount = 1;
|
||||
if (byWord && wordPattern != null) {
|
||||
Matcher m = wordPattern.matcher(text.substring(cursorPosition));
|
||||
if (m.find() && m.start() > 0) {
|
||||
moveAmount = m.start();
|
||||
} else if (m.find()) {
|
||||
moveAmount = m.start();
|
||||
}
|
||||
}
|
||||
return Math.min(cursorPosition + moveAmount, textLen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cursor position from mouse click coordinates
|
||||
*/
|
||||
public static int getPositionFromMouse(
|
||||
int mouseX, int mouseY,
|
||||
int areaX, int areaY,
|
||||
int gutterWidth,
|
||||
int lineHeight,
|
||||
int scrolledLine,
|
||||
double scrollFracOffset,
|
||||
List<LineData> lines,
|
||||
String text,
|
||||
FontWidthProvider fontWidth) {
|
||||
|
||||
int xMouse = mouseX - (areaX + gutterWidth + 1);
|
||||
int yMouse = mouseY - areaY - 1;
|
||||
|
||||
// Account for fractional scroll offset
|
||||
double fracPixels = scrollFracOffset * lineHeight;
|
||||
yMouse = (int) Math.round(yMouse + fracPixels);
|
||||
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
return text != null ? text.length() : 0;
|
||||
}
|
||||
|
||||
int visibleLines = Math.min(lines.size(), scrolledLine + 50); // Reasonable limit
|
||||
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
LineData data = lines.get(i);
|
||||
|
||||
if (i >= scrolledLine && i <= visibleLines) {
|
||||
int yPos = (i - scrolledLine) * lineHeight;
|
||||
|
||||
if (yMouse >= yPos && yMouse < yPos + lineHeight) {
|
||||
// Found the line, now find character position
|
||||
int lineWidth = 0;
|
||||
char[] chars = data.text.toCharArray();
|
||||
|
||||
for (int j = 1; j <= chars.length; j++) {
|
||||
int w = fontWidth.getWidth(data.text.substring(0, j));
|
||||
if (xMouse < lineWidth + (w - lineWidth) / 2) {
|
||||
return data.start + j - 1;
|
||||
}
|
||||
lineWidth = w;
|
||||
}
|
||||
|
||||
// Click past end of line - place cursor at end of text (not on newline)
|
||||
int posAfterChars = data.start + chars.length;
|
||||
return Math.min(posAfterChars, text.length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text != null ? text.length() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for font width measurement
|
||||
*/
|
||||
public interface FontWidthProvider {
|
||||
int getWidth(String text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get leading whitespace count of a line
|
||||
*/
|
||||
private static int getLineIndent(String lineText) {
|
||||
int indent = 0;
|
||||
for (int i = 0; i < lineText.length(); i++) {
|
||||
char c = lineText.charAt(i);
|
||||
if (c == ' ' || c == '\t') indent++;
|
||||
else break;
|
||||
}
|
||||
return indent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find line index containing given position
|
||||
*/
|
||||
public static int findLineIndex(int position, List<LineData> lines) {
|
||||
if (lines == null || lines.isEmpty()) return 0;
|
||||
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
LineData line = lines.get(i);
|
||||
boolean isLastLine = (i == lines.size() - 1);
|
||||
boolean isOnLine = isLastLine
|
||||
? (position >= line.start && position <= line.end)
|
||||
: (position >= line.start && position < line.end);
|
||||
if (isOnLine) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return lines.size() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find line data containing given position
|
||||
*/
|
||||
public static LineData findLineAt(int position, List<LineData> lines) {
|
||||
int idx = findLineIndex(position, lines);
|
||||
if (idx >= 0 && idx < lines.size()) {
|
||||
return lines.get(idx);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package noppes.npcs.client.gui.util.script;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper class for indentation-related operations.
|
||||
*/
|
||||
public class IndentHelper {
|
||||
|
||||
public static final int TAB_SIZE = 4;
|
||||
|
||||
/**
|
||||
* Get leading whitespace count of a line
|
||||
*/
|
||||
public static int getLineIndent(String lineText) {
|
||||
int leading = 0;
|
||||
for (int i = 0; i < lineText.length(); i++) {
|
||||
char c = lineText.charAt(i);
|
||||
if (c == ' ' || c == '\t') leading++;
|
||||
else break;
|
||||
}
|
||||
return leading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract leading whitespace as string
|
||||
*/
|
||||
public static String getIndentString(String lineText) {
|
||||
int indent = getLineIndent(lineText);
|
||||
return lineText.substring(0, indent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a string of n spaces
|
||||
*/
|
||||
public static String spaces(int n) {
|
||||
if (n <= 0) return "";
|
||||
char[] arr = new char[n];
|
||||
Arrays.fill(arr, ' ');
|
||||
return new String(arr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate expected indent for a line based on context
|
||||
*/
|
||||
public static int getExpectedIndent(LineData currentLine, List<LineData> allLines) {
|
||||
if (currentLine == null || allLines == null) return 0;
|
||||
|
||||
int idx = allLines.indexOf(currentLine);
|
||||
if (idx <= 0) return 0;
|
||||
|
||||
// Find previous non-empty line
|
||||
LineData prevLine = null;
|
||||
for (int i = idx - 1; i >= 0; i--) {
|
||||
LineData line = allLines.get(i);
|
||||
if (line.text.trim().length() > 0) {
|
||||
prevLine = line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (prevLine == null) return 0;
|
||||
|
||||
int prevIndent = getLineIndent(prevLine.text);
|
||||
String prevTrimmed = prevLine.text.trim();
|
||||
String currTrimmed = currentLine.text.trim();
|
||||
|
||||
// If current line starts with }, expected is one level back
|
||||
if (currTrimmed.startsWith("}")) {
|
||||
return Math.max(0, prevIndent - TAB_SIZE);
|
||||
}
|
||||
|
||||
// If previous line ends with {, expected is one level forward
|
||||
if (prevTrimmed.endsWith("{")) {
|
||||
return prevIndent + TAB_SIZE;
|
||||
}
|
||||
|
||||
return prevIndent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate auto-indent for Enter key
|
||||
*/
|
||||
public static String getAutoIndentForEnter(String lineText, int cursorInLine) {
|
||||
int leading = 0;
|
||||
while (leading < lineText.length() &&
|
||||
(lineText.charAt(leading) == ' ' || lineText.charAt(leading) == '\t')) {
|
||||
leading++;
|
||||
}
|
||||
|
||||
String baseIndent = lineText.substring(0, leading);
|
||||
int relativeCursor = Math.max(0, Math.min(cursorInLine, lineText.length()));
|
||||
String beforeCursor = lineText.substring(0, relativeCursor);
|
||||
|
||||
// Find last non-whitespace before cursor
|
||||
int lastNonWs = -1;
|
||||
for (int i = beforeCursor.length() - 1; i >= 0; i--) {
|
||||
if (!Character.isWhitespace(beforeCursor.charAt(i))) {
|
||||
lastNonWs = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add extra indent if last char is {
|
||||
// Only add extra indent if cursor is actually after the opening brace (not just in trailing whitespace)
|
||||
// Check that the last non-whitespace is at or after the line's indent position
|
||||
boolean opensBlock = lastNonWs >= 0 && lastNonWs >= leading && beforeCursor.charAt(lastNonWs) == '{';
|
||||
if (opensBlock) {
|
||||
return baseIndent + spaces(TAB_SIZE);
|
||||
}
|
||||
|
||||
return baseIndent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate target indent for Tab key
|
||||
*/
|
||||
public static int calculateTabTargetIndent(int currentIndent, int cursorColumn, boolean atTextStart) {
|
||||
if (atTextStart) {
|
||||
// At text start, move to next tab stop
|
||||
return ((currentIndent / TAB_SIZE) + 1) * TAB_SIZE;
|
||||
} else {
|
||||
// Inside indent, snap to nearest tab stop
|
||||
int remainder = currentIndent % TAB_SIZE;
|
||||
if (remainder == 0) {
|
||||
return currentIndent + TAB_SIZE;
|
||||
} else {
|
||||
int down = currentIndent - remainder;
|
||||
int up = currentIndent + (TAB_SIZE - remainder);
|
||||
int distDown = remainder;
|
||||
int distUp = TAB_SIZE - remainder;
|
||||
if (distUp <= distDown) return up;
|
||||
else return down;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate target indent for Shift+Tab
|
||||
*/
|
||||
public static int calculateShiftTabTargetIndent(int currentIndent) {
|
||||
return Math.max(0, ((currentIndent - 1) / TAB_SIZE) * TAB_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format entire text with proper indentation
|
||||
*/
|
||||
public static FormatResult formatText(String text, int cursorPosition) {
|
||||
String[] linesArr = text.split("\n", -1);
|
||||
StringBuilder out = new StringBuilder(text.length() + 32);
|
||||
|
||||
// Track cursor position in formatted output
|
||||
int cursorLine = -1;
|
||||
int cursorColInContent = 0;
|
||||
int lineStartPos = 0;
|
||||
|
||||
// Find cursor's line and column
|
||||
for (int li = 0; li < linesArr.length; li++) {
|
||||
String line = linesArr[li];
|
||||
int lineEndPos = lineStartPos + line.length();
|
||||
|
||||
if (cursorPosition >= lineStartPos && cursorPosition <= lineEndPos) {
|
||||
cursorLine = li;
|
||||
int leadingSpaces = line.length() - line.replaceAll("^[ \\t]+", "").length();
|
||||
cursorColInContent = Math.max(0, cursorPosition - lineStartPos - leadingSpaces);
|
||||
break;
|
||||
}
|
||||
lineStartPos = lineEndPos + 1;
|
||||
}
|
||||
|
||||
boolean inString = false;
|
||||
boolean escape = false;
|
||||
boolean inBlockComment = false;
|
||||
int depth = 0;
|
||||
int newCursorPos = 0;
|
||||
|
||||
for (int li = 0; li < linesArr.length; li++) {
|
||||
String line = linesArr[li];
|
||||
String trimmedLeading = line.replaceAll("^[ \\t]+", "");
|
||||
|
||||
int opens = 0, closes = 0;
|
||||
boolean startsWithClose = false;
|
||||
|
||||
// Parse line for braces
|
||||
for (int idx = 0; idx < line.length(); idx++) {
|
||||
char c = line.charAt(idx);
|
||||
char next = idx + 1 < line.length() ? line.charAt(idx + 1) : 0;
|
||||
|
||||
if (inString) {
|
||||
if (escape) { escape = false; }
|
||||
else if (c == '\\') { escape = true; }
|
||||
else if (c == '"') { inString = false; }
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inBlockComment) {
|
||||
if (c == '*' && next == '/') { inBlockComment = false; idx++; }
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '/' && next == '/') break;
|
||||
if (c == '/' && next == '*') { inBlockComment = true; idx++; continue; }
|
||||
if (c == '"') { inString = true; escape = false; continue; }
|
||||
|
||||
if (c == '{') {
|
||||
opens++;
|
||||
} else if (c == '}') {
|
||||
closes++;
|
||||
if (!startsWithClose && line.substring(0, idx).trim().isEmpty()) {
|
||||
startsWithClose = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int indentLevel = depth;
|
||||
if (startsWithClose) indentLevel = Math.max(0, indentLevel - 1);
|
||||
int targetIndent = indentLevel * TAB_SIZE;
|
||||
|
||||
if (li == cursorLine) {
|
||||
newCursorPos = out.length() + targetIndent + Math.min(cursorColInContent, trimmedLeading.length());
|
||||
}
|
||||
|
||||
out.append(spaces(targetIndent)).append(trimmedLeading);
|
||||
if (li < linesArr.length - 1) out.append('\n');
|
||||
|
||||
depth = Math.max(0, depth + opens - closes);
|
||||
}
|
||||
|
||||
return new FormatResult(out.toString(), newCursorPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of text formatting
|
||||
*/
|
||||
public static class FormatResult {
|
||||
public final String text;
|
||||
public final int cursorPosition;
|
||||
|
||||
public FormatResult(String text, int cursorPosition) {
|
||||
this.text = text;
|
||||
this.cursorPosition = cursorPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package noppes.npcs.client.gui.util.script;
|
||||
|
||||
/**
|
||||
* Manages smooth scrolling state and animation for the script text area.
|
||||
* Uses exponential smoothing for fluid scroll animations.
|
||||
*/
|
||||
public class ScrollState {
|
||||
// Current fractional scroll position (in lines)
|
||||
private double scrollPos = -1.0;
|
||||
// Target scroll position to animate towards
|
||||
private double targetScroll = 0.0;
|
||||
// Current scroll velocity for momentum calculations
|
||||
private double scrollVelocity = 0.0;
|
||||
// Last timestamp for delta time calculation
|
||||
private long lastScrollTime = 0L;
|
||||
// Integer line index for compatibility with rendering
|
||||
private int scrolledLine = 0;
|
||||
// Scrollbar drag offset when dragging thumb
|
||||
private int scrollbarDragOffset = 0;
|
||||
// Whether currently dragging the scrollbar
|
||||
private boolean clickScrolling = false;
|
||||
|
||||
// Animation parameters
|
||||
private static final double TAU = 0.1; // Time constant (~55ms feels snappy)
|
||||
private static final double SNAP_THRESHOLD = 0.01; // Snap to target when this close
|
||||
private static final double MAX_DT = 0.05; // Max delta time for stability
|
||||
|
||||
/**
|
||||
* Reset scroll state to initial values
|
||||
*/
|
||||
public void reset() {
|
||||
scrollPos = 0.0;
|
||||
targetScroll = 0.0;
|
||||
scrollVelocity = 0.0;
|
||||
scrolledLine = 0;
|
||||
lastScrollTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize scroll position if not already initialized
|
||||
*/
|
||||
public void initializeIfNeeded(int currentLine) {
|
||||
if (scrollPos < 0) {
|
||||
scrollPos = currentLine;
|
||||
lastScrollTime = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update scroll animation. Call once per frame.
|
||||
* @param maxScroll Maximum valid scroll position
|
||||
*/
|
||||
public void update(int maxScroll) {
|
||||
long nowMs = System.currentTimeMillis();
|
||||
double dt = Math.min(MAX_DT, (nowMs - lastScrollTime) / 1000.0);
|
||||
lastScrollTime = nowMs;
|
||||
|
||||
double dist = targetScroll - scrollPos;
|
||||
|
||||
// Snap to target if close enough
|
||||
if (Math.abs(dist) < SNAP_THRESHOLD) {
|
||||
scrollPos = targetScroll;
|
||||
scrollVelocity = 0.0;
|
||||
} else {
|
||||
// Exponential smoothing
|
||||
double alpha = 1.0 - Math.exp(-dt / Math.max(1e-6, TAU));
|
||||
double prev = scrollPos;
|
||||
scrollPos += dist * alpha;
|
||||
scrollVelocity = (scrollPos - prev) / (dt > 0 ? dt : 1e-6);
|
||||
|
||||
// Clamp overshoot
|
||||
if ((dist > 0 && scrollPos > targetScroll) || (dist < 0 && scrollPos < targetScroll)) {
|
||||
scrollPos = targetScroll;
|
||||
scrollVelocity = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep in bounds
|
||||
clampToBounds(maxScroll);
|
||||
|
||||
// Update integer line for rendering
|
||||
scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp scroll position to valid bounds
|
||||
*/
|
||||
public void clampToBounds(int maxScroll) {
|
||||
if (scrollPos < 0) scrollPos = 0;
|
||||
if (scrollPos > maxScroll) scrollPos = maxScroll;
|
||||
if (targetScroll < 0) targetScroll = 0;
|
||||
if (targetScroll > maxScroll) targetScroll = maxScroll;
|
||||
scrolledLine = Math.max(0, Math.min(scrolledLine, maxScroll));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply mouse wheel scroll
|
||||
* @param wheelDelta Positive = scroll up, negative = scroll down
|
||||
* @param maxScroll Maximum valid scroll position
|
||||
*/
|
||||
public void applyWheelScroll(int wheelDelta, int maxScroll) {
|
||||
double sign = Math.copySign(1, wheelDelta);
|
||||
targetScroll -= sign * 2.0; // 2 lines per wheel tick
|
||||
clampToBounds(maxScroll);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set target scroll directly (for scrollbar dragging)
|
||||
*/
|
||||
public void setTargetScroll(double target, int maxScroll) {
|
||||
targetScroll = Math.max(0, Math.min(target, maxScroll));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to make a specific line visible
|
||||
* @param lineIdx Line index to make visible
|
||||
* @param visibleLines Number of visible lines in viewport
|
||||
* @param maxScroll Maximum scroll position
|
||||
*/
|
||||
public void scrollToLine(int lineIdx, int visibleLines, int maxScroll) {
|
||||
int firstVisible = scrolledLine;
|
||||
int lastFullyVisible = scrolledLine + visibleLines;
|
||||
|
||||
if (lineIdx < firstVisible) {
|
||||
// Line is above viewport - scroll up
|
||||
targetScroll = lineIdx;
|
||||
} else if (lineIdx > lastFullyVisible) {
|
||||
// Line is below viewport - scroll down
|
||||
targetScroll = Math.min(lineIdx - visibleLines, maxScroll);
|
||||
}
|
||||
// If line is visible, don't change scroll
|
||||
}
|
||||
|
||||
// Getters
|
||||
public double getScrollPos() { return scrollPos; }
|
||||
public double getTargetScroll() { return targetScroll; }
|
||||
public int getScrolledLine() { return scrolledLine; }
|
||||
public double getScrollVelocity() { return scrollVelocity; }
|
||||
public boolean isClickScrolling() { return clickScrolling; }
|
||||
public int getScrollbarDragOffset() { return scrollbarDragOffset; }
|
||||
|
||||
/**
|
||||
* Get fractional offset for sub-pixel rendering
|
||||
*/
|
||||
public double getFractionalOffset() {
|
||||
return scrollPos - scrolledLine;
|
||||
}
|
||||
|
||||
// Setters for scrollbar interaction
|
||||
public void setClickScrolling(boolean scrolling) { this.clickScrolling = scrolling; }
|
||||
public void setScrollbarDragOffset(int offset) { this.scrollbarDragOffset = offset; }
|
||||
public void setScrolledLine(int line) { this.scrolledLine = line; }
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package noppes.npcs.client.gui.util.script;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Manages cursor position, text selection, and related operations.
|
||||
*/
|
||||
public class SelectionState {
|
||||
private int startSelection = 0;
|
||||
private int endSelection = 0;
|
||||
private int cursorPosition = 0;
|
||||
private long lastInputTime = 0L;
|
||||
|
||||
/**
|
||||
* Reset selection to a single point
|
||||
*/
|
||||
public void reset(int position) {
|
||||
startSelection = endSelection = cursorPosition = position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp all selection bounds to valid text range
|
||||
*/
|
||||
public void clamp(int textLength) {
|
||||
if (textLength <= 0) {
|
||||
startSelection = endSelection = cursorPosition = 0;
|
||||
return;
|
||||
}
|
||||
startSelection = Math.max(0, Math.min(startSelection, textLength));
|
||||
endSelection = Math.max(0, Math.min(endSelection, textLength));
|
||||
cursorPosition = Math.max(0, Math.min(cursorPosition, textLength));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cursor position, optionally extending selection
|
||||
*/
|
||||
public void setCursor(int position, int textLength, boolean extendSelection) {
|
||||
position = Math.max(0, Math.min(position, textLength));
|
||||
|
||||
if (position != cursorPosition) {
|
||||
if (!extendSelection) {
|
||||
endSelection = startSelection = cursorPosition = position;
|
||||
} else {
|
||||
int diff = cursorPosition - position;
|
||||
if (cursorPosition == startSelection) {
|
||||
startSelection -= diff;
|
||||
} else if (cursorPosition == endSelection) {
|
||||
endSelection -= diff;
|
||||
}
|
||||
|
||||
// Ensure start <= end
|
||||
if (startSelection > endSelection) {
|
||||
int temp = endSelection;
|
||||
endSelection = startSelection;
|
||||
startSelection = temp;
|
||||
}
|
||||
cursorPosition = position;
|
||||
}
|
||||
clamp(textLength);
|
||||
markActivity();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a range of text
|
||||
*/
|
||||
public void setSelection(int start, int end) {
|
||||
startSelection = Math.min(start, end);
|
||||
endSelection = Math.max(start, end);
|
||||
cursorPosition = endSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select all text
|
||||
*/
|
||||
public void selectAll(int textLength) {
|
||||
startSelection = cursorPosition = 0;
|
||||
endSelection = textLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's an active selection (not just cursor)
|
||||
*/
|
||||
public boolean hasSelection() {
|
||||
return startSelection != endSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selected text
|
||||
*/
|
||||
public String getSelectedText(String text) {
|
||||
if (!hasSelection() || text == null) return "";
|
||||
int start = Math.max(0, Math.min(startSelection, text.length()));
|
||||
int end = Math.max(0, Math.min(endSelection, text.length()));
|
||||
return text.substring(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text before selection/cursor
|
||||
*/
|
||||
public String getTextBefore(String text) {
|
||||
if (text == null || startSelection <= 0) return "";
|
||||
return text.substring(0, Math.min(startSelection, text.length()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text after selection/cursor
|
||||
*/
|
||||
public String getTextAfter(String text) {
|
||||
if (text == null) return "";
|
||||
int pos = Math.min(endSelection, text.length());
|
||||
return text.substring(pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find which line the cursor is on (0-indexed)
|
||||
*/
|
||||
public int getCursorLineIndex(List<LineData> lines, int textLength) {
|
||||
if (lines == null || lines.isEmpty()) return 0;
|
||||
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
LineData ld = lines.get(i);
|
||||
boolean isLastLine = (i == lines.size() - 1);
|
||||
boolean isOnLine = isLastLine
|
||||
? (cursorPosition >= ld.start && cursorPosition <= ld.end)
|
||||
: (cursorPosition >= ld.start && cursorPosition < ld.end);
|
||||
if (isOnLine) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return lines.size() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cursor is on a specific line, handling the last line specially
|
||||
* (last line doesn't have trailing newline, so use <= for end boundary)
|
||||
*/
|
||||
public boolean isCursorOnLine(int lineIndex, LineData line, int totalLines) {
|
||||
boolean isLastLine = (lineIndex == totalLines - 1);
|
||||
return isLastLine
|
||||
? (cursorPosition >= line.start && cursorPosition <= line.end)
|
||||
: (cursorPosition >= line.start && cursorPosition < line.end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find line containing the cursor
|
||||
*/
|
||||
public LineData findCurrentLine(List<LineData> lines) {
|
||||
if (lines == null) return null;
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
LineData line = lines.get(i);
|
||||
if (isCursorOnLine(i, line, lines.size())) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark user activity (for cursor blink timing)
|
||||
*/
|
||||
public void markActivity() {
|
||||
lastInputTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there was recent input (for cursor blink pause)
|
||||
*/
|
||||
public boolean hadRecentInput() {
|
||||
return System.currentTimeMillis() - lastInputTime < 500;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public int getStartSelection() { return startSelection; }
|
||||
public int getEndSelection() { return endSelection; }
|
||||
public int getCursorPosition() { return cursorPosition; }
|
||||
public long getLastInputTime() { return lastInputTime; }
|
||||
|
||||
// Direct setters (use with caution, prefer higher-level methods)
|
||||
public void setStartSelection(int pos) { this.startSelection = pos; }
|
||||
public void setEndSelection(int pos) { this.endSelection = pos; }
|
||||
public void setCursorPositionDirect(int pos) { this.cursorPosition = pos; }
|
||||
|
||||
/**
|
||||
* After text modification, update selection to new position
|
||||
*/
|
||||
public void afterTextInsert(int newPosition) {
|
||||
endSelection = startSelection = cursorPosition = newPosition;
|
||||
markActivity();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package noppes.npcs.client.gui.util.script;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Manages undo/redo stacks for the text editor.
|
||||
*/
|
||||
public class UndoManager {
|
||||
|
||||
/**
|
||||
* Stores a snapshot of text state for undo/redo
|
||||
*/
|
||||
public static class UndoState {
|
||||
public final String text;
|
||||
public final int cursorPosition;
|
||||
|
||||
public UndoState(String text, int cursorPosition) {
|
||||
this.text = text;
|
||||
this.cursorPosition = cursorPosition;
|
||||
}
|
||||
}
|
||||
|
||||
private final List<UndoState> undoStack = new ArrayList<>();
|
||||
private final List<UndoState> redoStack = new ArrayList<>();
|
||||
private boolean isUndoing = false;
|
||||
|
||||
// Optional: limit stack size to prevent memory issues
|
||||
private static final int MAX_STACK_SIZE = 100;
|
||||
|
||||
/**
|
||||
* Record current state before making a change
|
||||
*/
|
||||
public void recordState(String text, int cursorPosition) {
|
||||
if (isUndoing) return;
|
||||
|
||||
undoStack.add(new UndoState(text, cursorPosition));
|
||||
redoStack.clear();
|
||||
|
||||
// Trim stack if too large
|
||||
while (undoStack.size() > MAX_STACK_SIZE) {
|
||||
undoStack.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo to previous state
|
||||
* @param currentText Current text before undo
|
||||
* @param currentCursor Current cursor position
|
||||
* @return Previous state, or null if nothing to undo
|
||||
*/
|
||||
public UndoState undo(String currentText, int currentCursor) {
|
||||
if (undoStack.isEmpty()) return null;
|
||||
|
||||
isUndoing = true;
|
||||
redoStack.add(new UndoState(currentText, currentCursor));
|
||||
UndoState state = undoStack.remove(undoStack.size() - 1);
|
||||
isUndoing = false;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo to next state
|
||||
* @param currentText Current text before redo
|
||||
* @param currentCursor Current cursor position
|
||||
* @return Next state, or null if nothing to redo
|
||||
*/
|
||||
public UndoState redo(String currentText, int currentCursor) {
|
||||
if (redoStack.isEmpty()) return null;
|
||||
|
||||
isUndoing = true;
|
||||
undoStack.add(new UndoState(currentText, currentCursor));
|
||||
UndoState state = redoStack.remove(redoStack.size() - 1);
|
||||
isUndoing = false;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if undo is available
|
||||
*/
|
||||
public boolean canUndo() {
|
||||
return !undoStack.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if redo is available
|
||||
*/
|
||||
public boolean canRedo() {
|
||||
return !redoStack.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all history
|
||||
*/
|
||||
public void clear() {
|
||||
undoStack.clear();
|
||||
redoStack.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currently performing an undo/redo operation
|
||||
*/
|
||||
public boolean isUndoing() {
|
||||
return isUndoing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set undoing flag (for external control during setText)
|
||||
*/
|
||||
public void setUndoing(boolean undoing) {
|
||||
this.isUndoing = undoing;
|
||||
}
|
||||
|
||||
// For backward compatibility with existing code
|
||||
public List<UndoState> getUndoList() { return undoStack; }
|
||||
public List<UndoState> getRedoList() { return redoStack; }
|
||||
}
|
||||
Reference in new issue
Block a user