Files
CustomNPC-Plus/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java
T

1506 lines
61 KiB
Java
Raw Normal View History

package noppes.npcs.client.gui.util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.util.ChatAllowedCharacters;
import noppes.npcs.NoppesStringUtils;
import noppes.npcs.client.ClientProxy;
2025-12-16 18:33:55 +02:00
2025-12-17 09:53:32 +02:00
import noppes.npcs.client.gui.util.script.JavaTextContainer;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
2025-12-16 18:33:55 +02:00
2025-12-17 09:53:32 +02:00
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown;
public class GuiScriptTextArea extends GuiNpcTextField {
public int id;
public int x;
public int y;
public int width;
public int height;
private int cursorCounter;
private ITextChangeListener listener;
public String text = null;
2025-12-16 18:33:55 +02:00
private JavaTextContainer container = null;
public boolean active = false;
public boolean enabled = true;
public boolean visible = true;
public boolean clicked = false;
public boolean doubleClicked = false;
public boolean clickScrolling = false;
private int startSelection;
private int endSelection;
private int cursorPosition;
private int scrolledLine = 0;
private boolean enableCodeHighlighting = false;
public List<GuiScriptTextArea.UndoData> undoList = new ArrayList();
public List<GuiScriptTextArea.UndoData> redoList = new ArrayList();
public boolean undoing = false;
private long lastClicked = 0L;
2023-07-30 15:47:03 -04:00
// private static TrueTypeFont font = new TrueTypeFont(new Font("Arial Unicode MS", Font.PLAIN, ConfigClient.FontSize), 1);
public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) {
super(id, guiScreen, x, y, width, height, null);
this.id = id;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.undoing = true;
this.setText(text);
this.undoing = false;
}
2025-03-13 17:14:45 -04:00
public void drawTextBox(int xMouse, int yMouse) {
if (!visible)
return;
2025-10-15 06:28:43 -04:00
clampSelectionBounds();
drawRect(x - 1, y - 1, x + width + 1, y + height + 1, 0xffa0a0a0);
drawRect(x, y, x + width, y + height, 0xff000000);
container.visibleLines = height / container.lineHeight;
2025-03-13 17:14:45 -04:00
if (listener instanceof GuiNPCInterface) {
2024-05-26 18:10:37 -04:00
int k2 = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel();
2025-03-13 17:14:45 -04:00
if (k2 != 0) {
this.scrolledLine -= k2 / 40;
2024-05-22 19:05:41 -04:00
this.scrolledLine = Math.max(Math.min(this.scrolledLine, this.container.linesCount - this.height / this.container.lineHeight), 0);
}
}
2025-03-13 17:14:45 -04:00
if (clicked) {
clicked = Mouse.isButtonDown(0);
int i = getSelectionPos(xMouse, yMouse);
2025-03-13 17:14:45 -04:00
if (i != cursorPosition) {
if (doubleClicked) {
startSelection = endSelection = cursorPosition;
doubleClicked = false;
}
setCursor(i, true);
}
2025-03-13 17:14:45 -04:00
} else if (doubleClicked) {
doubleClicked = false;
}
2025-03-13 17:14:45 -04:00
if (clickScrolling) {
clickScrolling = Mouse.isButtonDown(0);
int diff = container.linesCount - container.visibleLines;
2025-03-13 17:14:45 -04:00
scrolledLine = Math.min(Math.max((int) (1f * diff * (yMouse - y) / height), 0), diff);
}
int startBracket = 0, endBracket = 0;
if (startSelection >= 0 && startSelection < text.length() &&
2025-07-23 14:22:52 -04:00
(endSelection - startSelection == 1 || startSelection == endSelection)) {
char c = text.charAt(startSelection);
int found = 0;
2025-03-13 17:14:45 -04:00
if (c == '{') {
found = findClosingBracket(text.substring(startSelection), '{', '}');
2025-03-13 17:14:45 -04:00
} else if (c == '[') {
found = findClosingBracket(text.substring(startSelection), '[', ']');
2025-03-13 17:14:45 -04:00
} else if (c == '(') {
found = findClosingBracket(text.substring(startSelection), '(', ')');
2025-03-13 17:14:45 -04:00
} else if (c == '}') {
found = findOpeningBracket(text.substring(0, startSelection + 1), '{', '}');
2025-03-13 17:14:45 -04:00
} else if (c == ']') {
found = findOpeningBracket(text.substring(0, startSelection + 1), '[', ']');
2025-03-13 17:14:45 -04:00
} else if (c == ')') {
found = findOpeningBracket(text.substring(0, startSelection + 1), '(', ')');
}
2025-03-13 17:14:45 -04:00
if (found != 0) {
startBracket = startSelection;
endBracket = startSelection + found;
}
}
2025-12-16 18:33:55 +02:00
List<JavaTextContainer.LineData> list = new ArrayList<>(container.lines);
2025-12-17 20:06:47 +02:00
// Build brace spans: {origDepth, open line, close line, adjustedDepth}
2025-12-17 18:13:29 +02:00
List<int[]> braceSpans = computeBraceSpans(text, list);
String wordHightLight = null;
2025-03-13 17:14:45 -04:00
if (startSelection != endSelection) {
Matcher m = container.regexWord.matcher(text);
2025-03-13 17:14:45 -04:00
while (m.find()) {
if (m.start() == startSelection && m.end() == endSelection) {
wordHightLight = text.substring(startSelection, endSelection);
}
}
}
2025-03-13 17:14:45 -04:00
for (int i = 0; i < list.size(); i++) {
LineData data = list.get(i);
String line = data.text;
int w = line.length();
2025-03-13 17:14:45 -04:00
if (startBracket != endBracket) {
if (startBracket >= data.start && startBracket < data.end) {
int s = ClientProxy.Font.width(line.substring(0, startBracket - data.start));
int e = ClientProxy.Font.width(line.substring(0, startBracket - data.start + 1)) + 1;
int posY = y + 1 + (i - scrolledLine) * container.lineHeight;
drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x9900cc00);
}
2025-03-13 17:14:45 -04:00
if (endBracket >= data.start && endBracket < data.end) {
int s = ClientProxy.Font.width(line.substring(0, endBracket - data.start));
int e = ClientProxy.Font.width(line.substring(0, endBracket - data.start + 1)) + 1;
int posY = y + 1 + (i - scrolledLine) * container.lineHeight;
drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x9900cc00);
}
}
2025-03-13 17:14:45 -04:00
if (i >= scrolledLine && i < scrolledLine + container.visibleLines) {
if (wordHightLight != null) {
Matcher m = container.regexWord.matcher(line);
2025-03-13 17:14:45 -04:00
while (m.find()) {
if (line.substring(m.start(), m.end()).equals(wordHightLight)) {
int s = ClientProxy.Font.width(line.substring(0, m.start()));
int e = ClientProxy.Font.width(line.substring(0, m.end())) + 1;
int posY = y + 1 + (i - scrolledLine) * container.lineHeight;
drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x99004c00);
}
}
}
2025-03-13 17:14:45 -04:00
if (startSelection != endSelection && endSelection > data.start && startSelection <= data.end) {
if (startSelection < data.end) {
int s = ClientProxy.Font.width(line.substring(0, Math.max(startSelection - data.start, 0)));
int e = ClientProxy.Font.width(line.substring(0, Math.min(endSelection - data.start, w))) + 1;
int posY = y + 1 + (i - scrolledLine) * container.lineHeight;
drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x990000ff);
}
}
int yPos = y + (i - scrolledLine) * container.lineHeight + 1;
2025-12-17 18:13:29 +02:00
// Draw indent guides once per visible block based on brace spans
if (i == scrolledLine && !braceSpans.isEmpty()) {
int visStart = scrolledLine;
int visEnd = Math.min(list.size() - 1, scrolledLine + container.visibleLines - 1);
for (int[] span : braceSpans) {
2025-12-17 20:06:47 +02:00
int originalDepth = span[0];
2025-12-17 18:13:29 +02:00
int openLine = span[1];
int closeLine = span[2];
2025-12-17 20:06:47 +02:00
int depth = span.length > 3 ? span[3] : originalDepth;
// Skip top-level (depth 1) using the original depth to avoid hiding nested guides when adjusted
if (originalDepth <= 1)
2025-12-17 18:13:29 +02:00
continue;
int startLine = openLine + 1; // start under the opening brace
int endLine = closeLine - 1; // stop before the closing brace
if (startLine > endLine)
continue;
if (endLine < visStart || startLine > visEnd)
continue;
int drawStart = Math.max(startLine, visStart);
int drawEnd = Math.min(endLine, visEnd);
// Compute horizontal position: 4 spaces per indent level, minus a tiny left offset
2025-12-17 20:06:47 +02:00
int safeDepth = Math.max(1, depth);
int spaces = (safeDepth - 1) * 4;
2025-12-17 18:13:29 +02:00
StringBuilder sb = new StringBuilder();
for (int k = 0; k < spaces; k++)
sb.append(' ');
int px = ClientProxy.Font.width(sb.toString());
int gx = x + 4 + px - 2; // shift left ~2px for the IntelliJ feel
int topY = y + (drawStart - scrolledLine) * container.lineHeight;
int bottomY = y + (drawEnd - scrolledLine + 1) * container.lineHeight - 2;
drawRect(gx, topY, gx + 1, bottomY, 0x33FFFFFF);
}
}
data.drawString(x + 1, yPos, 0xFFe0e0e0);
2025-12-17 14:33:17 +02:00
if (active && isEnabled() && (cursorCounter / 6) % 2 == 0 && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) {
int posX = x + ClientProxy.Font.width(
line.substring(0, Math.min(cursorPosition - data.start, line.length())));
drawRect(posX + 1, yPos, posX + 2, yPos + 1 + container.lineHeight, -3092272);
}
}
}
2025-03-13 17:14:45 -04:00
if (hasVerticalScrollbar()) {
Minecraft.getMinecraft().renderEngine.bindTexture(GuiCustomScroll.resource);
int sbSize = Math.max((int) (1f * container.visibleLines / container.linesCount * height), 2);
int posX = x + width - 6;
2025-03-13 17:14:45 -04:00
int posY = (int) (y + 1f * scrolledLine / container.linesCount * (height - 4)) + 1;
drawRect(posX, posY, posX + 5, posY + sbSize, 0xFFe0e0e0);
}
}
private int findClosingBracket(String str, char s, char e) {
int found = 0;
char[] chars = str.toCharArray();
2025-03-13 17:14:45 -04:00
for (int i = 0; i < chars.length; ++i) {
char c = chars[i];
2025-03-13 17:14:45 -04:00
if (c == s) {
++found;
2025-03-13 17:14:45 -04:00
} else if (c == e) {
--found;
2025-03-13 17:14:45 -04:00
if (found == 0) {
return i;
}
}
}
return 0;
}
private int findOpeningBracket(String str, char s, char e) {
int found = 0;
char[] chars = str.toCharArray();
2025-03-13 17:14:45 -04:00
for (int i = chars.length - 1; i >= 0; --i) {
char c = chars[i];
2025-03-13 17:14:45 -04:00
if (c == e) {
++found;
2025-03-13 17:14:45 -04:00
} else if (c == s) {
--found;
2025-03-13 17:14:45 -04:00
if (found == 0) {
return i - chars.length + 1;
}
}
}
return 0;
}
2025-12-17 20:06:47 +02:00
// Computes brace spans as {originalDepth, openLineIndex, closeLineIndex, adjustedDepth}, inclusive of brace lines
2025-12-17 18:13:29 +02:00
private List<int[]> computeBraceSpans(String fullText, List<LineData> lines) {
List<int[]> spans = new ArrayList<>();
if (fullText == null || fullText.isEmpty() || lines == null || lines.isEmpty())
return spans;
int lineIdx = 0;
int lineEnd = lines.get(0).end;
2025-12-17 18:35:26 +02:00
List<Integer> openStack = new ArrayList<>(); // line indices of unmatched opens
boolean inLineComment = false;
boolean inBlockComment = false;
boolean inString = false;
boolean escape = false;
char stringDelimiter = 0;
2025-12-17 18:13:29 +02:00
for (int pos = 0; pos < fullText.length(); pos++) {
while (lineIdx < lines.size() - 1 && pos >= lineEnd) {
lineIdx++;
lineEnd = lines.get(lineIdx).end;
}
char c = fullText.charAt(pos);
2025-12-17 18:35:26 +02:00
char next = pos + 1 < fullText.length() ? fullText.charAt(pos + 1) : 0;
if (inString) {
if (escape) {
escape = false;
} else if (c == '\\') {
escape = true;
} else if (c == stringDelimiter) {
inString = false;
2025-12-17 20:06:47 +02:00
} else if (c == '\n') {
// Treat unterminated strings as ending at EOL to avoid breaking later brace parsing
inString = false;
2025-12-17 18:35:26 +02:00
}
continue;
}
if (inBlockComment) {
if (c == '*' && next == '/') {
inBlockComment = false;
pos++;
}
continue;
}
if (inLineComment) {
if (c == '\n') {
inLineComment = false;
}
continue;
}
if (c == '/' && next == '/') {
inLineComment = true;
pos++;
continue;
}
if (c == '/' && next == '*') {
inBlockComment = true;
pos++;
continue;
}
if (c == '"' || c == '\'') {
inString = true;
stringDelimiter = c;
escape = false;
continue;
}
2025-12-17 18:13:29 +02:00
if (c == '{') {
openStack.add(lineIdx);
} else if (c == '}') {
if (!openStack.isEmpty()) {
int openLine = openStack.remove(openStack.size() - 1);
2025-12-17 18:35:26 +02:00
int spanDepth = openStack.size() + 1;
2025-12-17 18:13:29 +02:00
int closeLine = lineIdx;
spans.add(new int[]{spanDepth, openLine, closeLine});
}
}
}
2025-12-17 18:35:26 +02:00
2025-12-17 20:06:47 +02:00
// Normalize depths to reduce impact of unmatched opens without hiding nested guides
2025-12-17 18:35:26 +02:00
if (!openStack.isEmpty()) {
2025-12-17 20:06:47 +02:00
int baseline = openStack.size();
2025-12-17 18:35:26 +02:00
List<int[]> adjusted = new ArrayList<>(spans.size());
for (int[] span : spans) {
2025-12-17 20:06:47 +02:00
int adjustedDepth = Math.max(1, span[0] - baseline);
adjusted.add(new int[]{span[0], span[1], span[2], adjustedDepth});
2025-12-17 18:35:26 +02:00
}
spans = adjusted;
2025-12-17 20:06:47 +02:00
} else {
for (int idx = 0; idx < spans.size(); idx++) {
int[] span = spans.get(idx);
spans.set(idx, new int[]{span[0], span[1], span[2], span[0]});
}
2025-12-17 18:35:26 +02:00
}
2025-12-17 18:13:29 +02:00
return spans;
}
private int getSelectionPos(int xMouse, int yMouse) {
xMouse -= this.x + 1;
yMouse -= this.y + 1;
ArrayList list = new ArrayList(this.container.lines);
2025-03-13 17:14:45 -04:00
for (int i = 0; i < list.size(); ++i) {
LineData data = (LineData) list.get(i);
if (i >= this.scrolledLine && i < this.scrolledLine + this.container.visibleLines) {
int yPos = (i - this.scrolledLine) * this.container.lineHeight;
2025-03-13 17:14:45 -04:00
if (yMouse >= yPos && yMouse < yPos + this.container.lineHeight) {
int lineWidth = 0;
char[] chars = data.text.toCharArray();
2025-03-13 17:14:45 -04:00
for (int j = 1; j <= chars.length; ++j) {
int w = ClientProxy.Font.width(data.text.substring(0, j));
2025-03-13 17:14:45 -04:00
if (xMouse < lineWidth + (w - lineWidth) / 2) {
return data.start + j - 1;
}
lineWidth = w;
}
2025-12-17 15:13:38 +02:00
// Place cursor after the last visible character of the line.
// `data.end - 1` previously pointed at the newline for non-last lines
// which made clicks land on the newline rather than after the text.
// Use data.start + chars.length to return the position directly
// after the line's characters, clamped to the total text length.
int posAfterChars = data.start + chars.length;
return Math.min(posAfterChars, text.length());
}
}
}
return this.container.text.length();
}
@Override
public boolean textboxKeyTyped(char c, int i) {
2025-03-13 17:14:45 -04:00
if (!active)
return false;
2025-03-13 17:14:45 -04:00
if (this.isKeyComboCtrlA(i)) {
startSelection = cursorPosition = 0;
endSelection = text.length();
return true;
}
2025-03-13 17:14:45 -04:00
if (!isEnabled())
return false;
2025-12-17 14:33:17 +02:00
LineData currentLine = null;
for (LineData line : this.container.lines) {
if (startSelection >= line.start && startSelection <= line.end) {
currentLine = line;
break;
}
}
String original = text;
2025-03-13 17:14:45 -04:00
if (i == Keyboard.KEY_LEFT) {
int j = 1;
2025-03-13 17:14:45 -04:00
if (isCtrlKeyDown()) {
Matcher m = container.regexWord.matcher(text.substring(0, cursorPosition));
2025-03-13 17:14:45 -04:00
while (m.find()) {
if (m.start() == m.end())
continue;
j = cursorPosition - m.start();
}
}
2025-12-17 14:33:17 +02:00
int newPos = Math.max(cursorPosition - j, 0);
setCursor(newPos, GuiScreen.isShiftKeyDown());
return true;
}
2025-03-13 17:14:45 -04:00
if (i == Keyboard.KEY_RIGHT) {
int j = 1;
2025-03-13 17:14:45 -04:00
if (isCtrlKeyDown()) {
Matcher m = container.regexWord.matcher(text.substring(cursorPosition));
2025-03-13 17:14:45 -04:00
if (m.find() && m.start() > 0 || m.find()) {
j = m.start();
}
}
2025-12-17 20:06:47 +02:00
int newPos = Math.min(cursorPosition + j, text.length());
2025-12-17 14:33:17 +02:00
setCursor(newPos, GuiScreen.isShiftKeyDown());
return true;
}
2025-03-13 17:14:45 -04:00
if (i == Keyboard.KEY_UP) {
setCursor(cursorUp(), GuiScreen.isShiftKeyDown());
return true;
}
2025-03-13 17:14:45 -04:00
if (i == Keyboard.KEY_DOWN) {
setCursor(cursorDown(), GuiScreen.isShiftKeyDown());
return true;
}
2025-03-13 17:14:45 -04:00
if (i == Keyboard.KEY_DELETE) {
String s = getSelectionAfterText();
2025-03-13 17:14:45 -04:00
if (!s.isEmpty() && startSelection == endSelection)
s = s.substring(1);
setText(getSelectionBeforeText() + s);
endSelection = cursorPosition = startSelection;
return true;
}
2025-03-13 17:14:45 -04:00
if (isKeyComboCtrlBackspace(i)) {
2023-07-30 15:47:03 -04:00
String s = getSelectionBeforeText();
2025-03-13 17:14:45 -04:00
if (startSelection > 0 && startSelection == endSelection) {
2023-07-30 15:47:03 -04:00
int nearestCondition = cursorPosition;
int g;
boolean cursorInWhitespace = Character.isWhitespace(s.charAt(cursorPosition - 1));
2025-03-13 17:14:45 -04:00
if (cursorInWhitespace) {
2023-07-30 15:47:03 -04:00
// Find the nearest word if we are starting in whitespace
for (g = cursorPosition - 1; g >= 0; g--) {
char currentChar = s.charAt(g);
if (!Character.isWhitespace(currentChar)) {
nearestCondition = g;
break;
}
2025-03-13 17:14:45 -04:00
if (g == 0) {
2023-07-30 15:47:03 -04:00
nearestCondition = 0;
}
}
2025-03-13 17:14:45 -04:00
} else {
2023-07-30 15:47:03 -04:00
// Find the nearest blank space or new line
for (g = cursorPosition - 1; g >= 0; g--) {
char currentChar = s.charAt(g);
2025-03-13 17:14:45 -04:00
if (Character.isWhitespace(currentChar) || currentChar == '\n') {
2023-07-30 15:47:03 -04:00
nearestCondition = g;
break;
}
2025-03-13 17:14:45 -04:00
if (g == 0) {
2023-07-30 15:47:03 -04:00
nearestCondition = 0;
}
}
}
// Remove all text to the left up to the nearest boundary
s = s.substring(0, nearestCondition);
startSelection -= (cursorPosition - nearestCondition);
}
setText(s + getSelectionAfterText());
endSelection = cursorPosition = startSelection;
return true;
}
2025-03-13 17:14:45 -04:00
if (i == Keyboard.KEY_BACK) {
// Handle selection deletion first
if (startSelection != endSelection) {
String s = getSelectionBeforeText();
setText(s + getSelectionAfterText());
endSelection = cursorPosition = startSelection;
return true;
}
// Nothing to delete if at start
if (startSelection <= 0) {
return true;
}
// Find current line for indent-aware backspace
LineData curr = null;
for (LineData line : container.lines) {
if (cursorPosition >= line.start && cursorPosition <= line.end) {
curr = line;
break;
}
}
// Indent-aware backspace: if cursor is at or before the EXPECTED indent, merge with previous line
// If cursor is after expected indent (even in extra whitespace), do normal backspace
if (curr != null && curr.start > 0) {
int col = cursorPosition - curr.start;
int actualIndent = getLineIndent(curr.text);
int expectedIndent = getExpectedIndent(curr);
// Only trigger smart merge if cursor is at or before the expected indent position
if (col <= expectedIndent) {
boolean lineHasContent = curr.text.trim().length() > 0;
int newlinePos = curr.start - 1; // Position of newline before this line
if (!lineHasContent) {
// Empty/whitespace-only line: remove the entire line including its trailing newline
int removeEnd = text.indexOf('\n', curr.start);
if (removeEnd == -1) {
removeEnd = text.length();
} else {
removeEnd = removeEnd + 1; // Include the newline
}
String before = text.substring(0, curr.start);
String after = removeEnd <= text.length() ? text.substring(removeEnd) : "";
setText(before + after);
// Cursor goes to end of previous line (at the newline position, which is now end of prev line content)
int newCursor = Math.max(0, curr.start - 1);
endSelection = cursorPosition = startSelection = newCursor;
return true;
} else {
// Line has content: merge with previous line
// Remove newline + indent, keep content
int contentStart = curr.start + actualIndent;
String before = text.substring(0, newlinePos);
String content = contentStart <= text.length() ? text.substring(contentStart) : "";
// Determine if we need spacing between merged content
String spacer = "";
if (before.length() > 0 && content.length() > 0) {
char lastChar = before.charAt(before.length() - 1);
char firstChar = content.charAt(0);
// Add space unless previous ends with whitespace/open bracket or current starts with close bracket/punctuation
if (!Character.isWhitespace(lastChar) &&
lastChar != '{' && lastChar != '(' && lastChar != '[' &&
firstChar != '}' && firstChar != ')' && firstChar != ']' &&
firstChar != ';' && firstChar != ',' && firstChar != '.' &&
firstChar != '\n') {
spacer = " ";
}
2025-12-17 20:06:47 +02:00
}
setText(before + spacer + content);
int newCursor = before.length() + spacer.length();
endSelection = cursorPosition = startSelection = newCursor;
2025-12-17 20:06:47 +02:00
return true;
}
}
}
// Auto-pair deletion: if deleting an opening bracket/quote and next char is the matching closer
if (startSelection > 0 && startSelection < text.length()) {
char prev = text.charAt(startSelection - 1);
char nextc = text.charAt(startSelection);
if ((prev == '(' && nextc == ')') ||
(prev == '[' && nextc == ']') ||
(prev == '{' && nextc == '}') ||
(prev == '\'' && nextc == '\'') ||
(prev == '"' && nextc == '"')) {
String before = text.substring(0, startSelection - 1);
String after = startSelection + 1 < text.length() ? text.substring(startSelection + 1) : "";
setText(before + after);
startSelection -= 1;
endSelection = cursorPosition = startSelection;
return true;
}
}
// Normal backspace: delete one character
String s = getSelectionBeforeText();
s = s.substring(0, s.length() - 1);
startSelection--;
setText(s + getSelectionAfterText());
endSelection = cursorPosition = startSelection;
return true;
}
2025-03-13 17:14:45 -04:00
if (this.isKeyComboCtrlX(i)) {
if (startSelection != endSelection) {
NoppesStringUtils.setClipboardContents(text.substring(startSelection, endSelection));
String s = getSelectionBeforeText();
setText(s + getSelectionAfterText());
endSelection = startSelection = cursorPosition = s.length();
}
return true;
}
2025-03-13 17:14:45 -04:00
if (this.isKeyComboCtrlC(i)) {
if (startSelection != endSelection) {
NoppesStringUtils.setClipboardContents(text.substring(startSelection, endSelection));
}
return true;
}
2025-03-13 17:14:45 -04:00
if (this.isKeyComboCtrlV(i)) {
addText(NoppesStringUtils.getClipboardContents());
return true;
}
2025-03-13 17:14:45 -04:00
if (i == Keyboard.KEY_Z && isCtrlKeyDown()) {
if (undoList.isEmpty())
return false;
undoing = true;
redoList.add(new UndoData(this.text, this.cursorPosition));
UndoData data = undoList.remove(undoList.size() - 1);
setText(data.text);
endSelection = startSelection = cursorPosition = data.cursorPosition;
undoing = false;
return true;
}
2025-03-13 17:14:45 -04:00
if (i == Keyboard.KEY_Y && isCtrlKeyDown()) {
if (redoList.isEmpty())
return false;
undoing = true;
undoList.add(new UndoData(this.text, this.cursorPosition));
UndoData data = redoList.remove(redoList.size() - 1);
setText(data.text);
endSelection = startSelection = cursorPosition = data.cursorPosition;
undoing = false;
return true;
}
2025-03-13 17:14:45 -04:00
if (i == Keyboard.KEY_TAB) {
2025-12-17 20:06:47 +02:00
boolean shift = isShiftKeyDown();
if (shift) {
handleShiftTab();
} else {
handleTab();
}
}
if (i == Keyboard.KEY_F && isCtrlKeyDown()) {
formatText();
return true;
}
2025-03-13 17:14:45 -04:00
if (i == Keyboard.KEY_RETURN) {
2025-12-17 20:06:47 +02:00
if (cursorPosition > 0 && cursorPosition <= text.length() && text.charAt(cursorPosition - 1) == '{') {
String indent = getIndentCurrentLine();
String childIndent = indent + " ";
String before = getSelectionBeforeText();
String after = getSelectionAfterText();
// Find the next non-whitespace character and its indent to decide if this brace is already closed at the same scope.
int nextNonWsIndex = -1;
for (int idx = 0; idx < after.length(); idx++) {
char ch = after.charAt(idx);
if (!Character.isWhitespace(ch)) {
nextNonWsIndex = idx;
break;
}
}
boolean hasMatchingCloseSameIndent = false;
if (nextNonWsIndex >= 0 && after.charAt(nextNonWsIndex) == '}') {
// Compute indent of that closing brace
int lineStart = after.lastIndexOf('\n', nextNonWsIndex);
int indentLen = 0;
for (int k = lineStart + 1; k < after.length(); k++) {
char ch = after.charAt(k);
if (ch == ' ') indentLen++; else if (ch == '\t') indentLen += 4; else break;
if (k == nextNonWsIndex) break;
}
if (indentLen == indent.length()) {
hasMatchingCloseSameIndent = true;
}
}
if (hasMatchingCloseSameIndent) {
addText("\n" + childIndent);
} else {
String insert = "\n" + childIndent + "\n" + indent + "}";
setText(before + insert + after);
int newCursor = before.length() + 1 + childIndent.length();
startSelection = endSelection = cursorPosition = newCursor;
}
} else {
addText(Character.toString('\n') + getAutoIndentForEnter());
}
}
2025-03-13 17:14:45 -04:00
if (ChatAllowedCharacters.isAllowedCharacter(c)) {
2025-12-17 20:06:47 +02:00
String before = getSelectionBeforeText();
String after = getSelectionAfterText();
// If typing a closing char and the next char is already that closer, skip over it
if ((c == ')' || c == ']' || c == '"' || c == '\'' ) && after.length() > 0 && after.charAt(0) == c) {
// move cursor forward over existing closer
startSelection = endSelection = cursorPosition = before.length() + 1;
return true;
}
if (c == '"') {
setText(before + "\"\"" + after);
startSelection = endSelection = cursorPosition = before.length() + 1;
return true;
}
if (c == '\'') {
setText(before + "''" + after);
startSelection = endSelection = cursorPosition = before.length() + 1;
return true;
}
if (c == '[') {
setText(before + "[]" + after);
startSelection = endSelection = cursorPosition = before.length() + 1;
return true;
}
if (c == '(') {
setText(before + "()" + after);
startSelection = endSelection = cursorPosition = before.length() + 1;
return true;
}
addText(Character.toString(c));
}
2025-12-17 09:53:32 +02:00
if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) {
if (startSelection != endSelection) {
toggleCommentSelection();
} else {
toggleCommentLineAtCursor();
2025-12-17 09:53:32 +02:00
}
return true;
2025-12-17 09:53:32 +02:00
}
if (i == Keyboard.KEY_D && isCtrlKeyDown()) {
if (startSelection != endSelection) {
// Handle multi-line selection duplication
LineData firstLine = null, lastLine = null;
for (LineData line : container.lines) {
if (line.end > startSelection && line.start < endSelection) {
if (firstLine == null) firstLine = line;
lastLine = line;
}
}
if (firstLine != null && lastLine != null) {
String selectedText = text.substring(firstLine.start, lastLine.end);
String insertText = "\n" + selectedText;
// Save selection before setText
int savedStart = startSelection;
int savedEnd = endSelection;
setText(text.substring(0, lastLine.end) + insertText + text.substring(lastLine.end));
// Move cursor to start of duplicated block
setCursor(lastLine.end + 1, false);
// Restore selection
startSelection = savedStart;
endSelection = savedEnd;
2025-12-17 09:53:32 +02:00
return true;
}
} else {
// Single line duplication (existing logic)
for (LineData line : container.lines) {
if (cursorPosition >= line.start && cursorPosition <= line.end) {
int lineStart = line.start, lineEnd = line.end;
String lineText = text.substring(lineStart, lineEnd);
String insertText = "\n" + lineText;
// Save selection before setText
int savedStart = startSelection;
int savedEnd = endSelection;
setText(text.substring(0, lineEnd) + insertText + text.substring(lineEnd));
// Move cursor to start of duplicated line
setCursor(lineEnd + 1, false);
// Restore selection
startSelection = savedStart;
endSelection = savedEnd;
return true;
}
}
2025-12-17 09:53:32 +02:00
}
}
return true;
}
2025-03-13 17:14:45 -04:00
private boolean isShiftKeyDown() {
return Keyboard.isKeyDown(42) || Keyboard.isKeyDown(54);
}
private void toggleCommentSelection() {
StringBuilder newText = new StringBuilder(text.length() + 100);
int prevEnd = 0;
int newStart = -1; //calculates expanded startSelection
int newEnd = -1; //calculates expanded endSelection
int newIndex = 0;
for (LineData line : container.lines) {
int safeLineStart = Math.max(0, Math.min(line.start, text.length()));
int safeLineEnd = Math.max(0, Math.min(line.end, text.length()));
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;
}
if (safeLineEnd > startSelection && safeLineStart < endSelection) {
String lineText = text.substring(safeLineStart, safeLineEnd);
CommentToggleInfo ti = toggleLine(lineText);
String newLineText = ti.text;
int nonWs = ti.nonWs;
int delta = ti.delta;
if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) {
int offsetInOldStart = startSelection - safeLineStart;
int newOffsetStart = offsetInOldStart;
if (offsetInOldStart >= nonWs) {
newOffsetStart = offsetInOldStart + delta;
}
newStart = newIndex + Math.max(0, newOffsetStart);
}
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 {
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;
}
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;
}
if (newStart == -1) newStart = Math.max(0, Math.min(startSelection, newText.length()));
if (newEnd == -1) newEnd = Math.max(0, Math.min(endSelection, newText.length()));
setText(newText.toString());
startSelection = newStart;
endSelection = newEnd;
cursorPosition = endSelection;
}
private void toggleCommentLineAtCursor() {
for (LineData line : container.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);
CommentToggleInfo ti = toggleLine(lineText);
String newLineText = ti.text;
int nonWs = ti.nonWs;
boolean hasComment = ti.delta < 0;
String before = text.substring(0, lineStart);
String after = lineEnd <= text.length() ? text.substring(lineEnd) : "";
setText(before + newLineText + after);
int cursorDelta = hasComment ? -2 : 2;
int newCursor = cursorPosition + cursorDelta;
if (cursorPosition < lineStart + nonWs + (hasComment ? 2 : 0)) newCursor = cursorPosition;
setCursor(Math.max(lineStart, newCursor), false);
return;
}
}
}
2025-03-13 17:14:45 -04:00
private boolean isAltKeyDown() {
return Keyboard.isKeyDown(56) || Keyboard.isKeyDown(184);
}
2025-03-13 17:14:45 -04:00
private boolean isKeyComboCtrlX(int keyID) {
return keyID == 45 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown();
}
2025-03-13 17:14:45 -04:00
private boolean isKeyComboCtrlBackspace(int keyID) {
2023-07-30 15:47:03 -04:00
return keyID == Keyboard.KEY_BACK && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown();
}
2025-03-13 17:14:45 -04:00
private boolean isKeyComboCtrlV(int keyID) {
return keyID == 47 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown();
}
2025-03-13 17:14:45 -04:00
private boolean isKeyComboCtrlC(int keyID) {
return keyID == 46 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown();
}
2025-03-13 17:14:45 -04:00
private boolean isKeyComboCtrlA(int keyID) {
return keyID == 30 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown();
}
private String getIndentCurrentLine() {
Iterator var1 = this.container.lines.iterator();
LineData data;
do {
2025-03-13 17:14:45 -04:00
if (!var1.hasNext()) {
return "";
}
2025-03-13 17:14:45 -04:00
data = (LineData) var1.next();
} while (this.cursorPosition <= data.start || this.cursorPosition > data.end);
int i;
2025-03-13 17:14:45 -04:00
for (i = 0; i < data.text.length() && data.text.charAt(i) == 32; ++i) {
;
}
return data.text.substring(0, i);
}
2025-12-17 18:35:26 +02:00
private String getAutoIndentForEnter() {
LineData currentLine = null;
for (LineData line : this.container.lines) {
if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) {
currentLine = line;
break;
}
}
if (currentLine == null) {
return "";
}
String lineText = currentLine.text;
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(this.cursorPosition - currentLine.start, lineText.length()));
String beforeCursor = lineText.substring(0, relativeCursor);
// Find last non-whitespace before cursor
2025-12-17 18:35:26 +02:00
int lastNonWs = -1;
for (int idx = beforeCursor.length() - 1; idx >= 0; idx--) {
if (!Character.isWhitespace(beforeCursor.charAt(idx))) {
lastNonWs = idx;
break;
}
}
// 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) == '{';
2025-12-17 18:35:26 +02:00
if (opensBlock) {
return baseIndent + " ";
}
return baseIndent;
}
2025-12-17 20:06:47 +02:00
private int getLineIndent(String lineText) {
int leading = 0;
while (leading < lineText.length() && (lineText.charAt(leading) == ' ' || lineText.charAt(
leading) == '\t')) {
leading++;
}
return leading;
}
2025-12-17 23:51:28 +02:00
// Calculate the expected/proper indent for the current line based on surrounding context
private int getExpectedIndent(LineData currentLine) {
if (currentLine == null) {
return 0;
}
int idx = container.lines.indexOf(currentLine);
if (idx <= 0) {
return 0; // First line has no expected indent
}
// Find the previous non-empty line
LineData prevLine = null;
for (int i = idx - 1; i >= 0; i--) {
LineData line = container.lines.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 indent is one level back
if (currTrimmed.startsWith("}")) {
return Math.max(0, prevIndent - getTabSize());
}
// If previous line ends with {, expected indent is one level forward
if (prevTrimmed.endsWith("{")) {
return prevIndent + getTabSize();
}
// Otherwise, expected indent matches previous line
return prevIndent;
}
2025-12-17 20:06:47 +02:00
private int getTabSize() {
return 4;
}
private String repeatSpace(int count) {
if (count <= 0)
return "";
char[] arr = new char[count];
java.util.Arrays.fill(arr, ' ');
return new String(arr);
}
private void formatText() {
// Find which line the cursor is on and calculate position within that line (after stripping indent)
int cursorLine = -1;
int cursorColInContent = 0; // position in line after removing leading whitespace
int lineStartPos = 0;
2025-12-17 20:06:47 +02:00
String[] linesArr = text.split("\n", -1);
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; // +1 for newline
}
2025-12-17 20:06:47 +02:00
StringBuilder out = new StringBuilder(text.length() + 32);
boolean inString = false;
boolean escape = false;
boolean inBlockComment = false;
int depth = 0;
int tab = getTabSize();
int newCursorPos = 0;
2025-12-17 20:06:47 +02:00
for (int li = 0; li < linesArr.length; li++) {
String line = linesArr[li];
String trimmedLeading = line.replaceAll("^[ \t]+", "");
int lineIndentDec = 0;
int opens = 0, closes = 0;
boolean startsWithClose = false;
// First pass to determine startsWithClose and brace counts ignoring strings/comments
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; // rest of line is comment
}
if (c == '/' && next == '*') {
inBlockComment = true;
idx++;
continue;
}
if (c == '"') {
inString = true;
escape = false;
continue;
}
if (c == '{') {
opens++;
} else if (c == '}') {
closes++;
if (!startsWithClose) {
String prefix = line.substring(0, idx);
if (prefix.trim().isEmpty()) {
startsWithClose = true;
}
}
}
}
int indentLevel = depth;
if (startsWithClose)
indentLevel = Math.max(0, indentLevel - 1);
int targetIndent = indentLevel * tab;
// Track cursor position in the formatted output
if (li == cursorLine) {
newCursorPos = out.length() + targetIndent + Math.min(cursorColInContent, trimmedLeading.length());
}
2025-12-17 20:06:47 +02:00
out.append(repeatSpace(targetIndent)).append(trimmedLeading);
if (li < linesArr.length - 1)
out.append('\n');
depth = Math.max(0, depth + opens - closes);
}
setText(out.toString());
// Restore cursor position
if (cursorLine >= 0) {
newCursorPos = Math.max(0, Math.min(newCursorPos, this.text.length()));
startSelection = endSelection = cursorPosition = newCursorPos;
}
2025-12-17 20:06:47 +02:00
}
private void handleTab() {
LineData currentLine = null;
for (LineData line : this.container.lines) {
if (this.cursorPosition >= line.start && this.cursorPosition < line.end) {
2025-12-17 20:06:47 +02:00
currentLine = line;
break;
}
}
if (currentLine == null) {
addText(" ");
return;
}
int tab = getTabSize();
int indentLen = getLineIndent(currentLine.text);
int textStartPos = currentLine.start + indentLen;
if (this.cursorPosition <= textStartPos) {
// Cursor before any text: if cursor is exactly at text start, move forward to next tab stop.
// If cursor is inside leading whitespace (before text start), choose nearest tab stop (tie -> forward).
int targetIndent;
if (this.cursorPosition == textStartPos) {
targetIndent = ((indentLen / tab) + 1) * tab;
} else {
int remainder = indentLen % tab;
if (remainder == 0) {
targetIndent = indentLen + tab; // already aligned -> next level
} else {
int down = indentLen - remainder;
int up = indentLen + (tab - remainder);
int distDown = remainder;
int distUp = tab - remainder;
if (distUp < distDown) targetIndent = up;
else if (distUp > distDown) targetIndent = down;
else targetIndent = up; // tie -> forward
}
}
if (targetIndent < 0) targetIndent = 0;
String newIndent = repeatSpace(targetIndent);
String rest = currentLine.text.substring(indentLen);
String before = text.substring(0, currentLine.start);
int contentEnd = Math.min(currentLine.start + currentLine.text.length(), text.length());
int sepEnd = Math.min(currentLine.end, text.length());
String sep = contentEnd < sepEnd ? text.substring(contentEnd, sepEnd) : "";
String after = text.substring(sepEnd);
setText(before + newIndent + rest + sep + after);
int newCursor = currentLine.start + targetIndent;
startSelection = endSelection = cursorPosition = Math.min(newCursor, this.text.length());
} else {
// Cursor is after start of text: insert spaces at cursor to move following text to next tab stop
int column = this.cursorPosition - currentLine.start;
int targetColumn = ((column / tab) + 1) * tab;
int toInsert = Math.max(0, targetColumn - column);
if (toInsert > 0) {
String spaces = repeatSpace(toInsert);
addText(spaces);
}
}
}
private void handleShiftTab() {
LineData currentLine = null;
for (LineData line : this.container.lines) {
if (this.cursorPosition >= line.start && this.cursorPosition < line.end) {
2025-12-17 20:06:47 +02:00
currentLine = line;
break;
}
}
if (currentLine == null)
return;
int tab = getTabSize();
int indentLen = getLineIndent(currentLine.text);
int textStartPos = currentLine.start + indentLen;
if (this.cursorPosition <= textStartPos) {
// Cursor before any text: reduce leading indent to previous tab stop
int targetIndent = Math.max(0, ((indentLen - 1) / tab) * tab);
String newIndent = repeatSpace(targetIndent);
String rest = currentLine.text.substring(indentLen);
String before = text.substring(0, currentLine.start);
int contentEnd = Math.min(currentLine.start + currentLine.text.length(), text.length());
int sepEnd = Math.min(currentLine.end, text.length());
String sep = contentEnd < sepEnd ? text.substring(contentEnd, sepEnd) : "";
String after = text.substring(sepEnd);
setText(before + newIndent + rest + sep + after);
int newCursor = currentLine.start + targetIndent;
startSelection = endSelection = cursorPosition = Math.min(newCursor, this.text.length());
} else {
// Cursor after start of text: remove up to previous tab stop worth of spaces immediately before cursor
int column = this.cursorPosition - currentLine.start;
int mod = column % tab;
int toRemove = mod == 0 ? tab : mod;
int removed = 0;
int pos = this.cursorPosition - 1;
while (pos >= currentLine.start && removed < toRemove && text.charAt(pos) == ' ') {
pos--;
removed++;
}
if (removed > 0) {
int removeStart = pos + 1;
String before = text.substring(0, removeStart);
String after = text.substring(this.cursorPosition);
setText(before + after);
int newCursor = removeStart;
startSelection = endSelection = cursorPosition = Math.min(newCursor, this.text.length());
}
}
}
private void setCursor(int i, boolean select) {
i = Math.min(Math.max(i, 0), this.text.length());
2025-03-13 17:14:45 -04:00
if (i != this.cursorPosition) {
if (!select) {
this.endSelection = this.startSelection = this.cursorPosition = i;
} else {
int diff = this.cursorPosition - i;
2025-03-13 17:14:45 -04:00
if (this.cursorPosition == this.startSelection) {
this.startSelection -= diff;
2025-03-13 17:14:45 -04:00
} else if (this.cursorPosition == this.endSelection) {
this.endSelection -= diff;
}
2025-03-13 17:14:45 -04:00
if (this.startSelection > this.endSelection) {
int j = this.endSelection;
this.endSelection = this.startSelection;
this.startSelection = j;
}
this.cursorPosition = i;
}
2025-10-15 06:28:43 -04:00
clampSelectionBounds();
}
}
private void addText(String s) {
this.setText(this.getSelectionBeforeText() + s + this.getSelectionAfterText());
this.endSelection = this.startSelection = this.cursorPosition = this.startSelection + s.length();
}
private int cursorUp() {
2025-03-13 17:14:45 -04:00
for (int i = 0; i < this.container.lines.size(); ++i) {
LineData data = (LineData) this.container.lines.get(i);
if (this.cursorPosition >= data.start && this.cursorPosition < data.end) {
if (i == 0) {
return 0;
}
2025-12-17 20:06:47 +02:00
int column = this.cursorPosition - data.start;
LineData target = this.container.lines.get(i - 1);
int targetPos = this.getSelectionPos(this.x + 1 + ClientProxy.Font.width(data.text.substring(0, column)), this.y + 1 + (i - 1 - this.scrolledLine) * this.container.lineHeight);
int targetIndent = getLineIndent(target.text);
int minPos = target.start + Math.min(targetIndent, Math.max(0, target.text.length()));
return Math.max(minPos, targetPos);
}
}
return 0;
}
private int cursorDown() {
2025-12-17 14:33:17 +02:00
if (cursorPosition == text.length())
return cursorPosition;
2025-03-13 17:14:45 -04:00
for (int i = 0; i < this.container.lines.size(); ++i) {
LineData data = (LineData) this.container.lines.get(i);
if (this.cursorPosition >= data.start && this.cursorPosition < data.end) {
2025-12-17 20:06:47 +02:00
int column = this.cursorPosition - data.start;
LineData target = this.container.lines.get(Math.min(i + 1, this.container.lines.size() - 1));
int targetPos = this.getSelectionPos(this.x + 1 + ClientProxy.Font.width(data.text.substring(0, column)), this.y + 1 + (i + 1 - this.scrolledLine) * this.container.lineHeight);
int targetIndent = getLineIndent(target.text);
int minPos = target.start + Math.min(targetIndent, Math.max(0, target.text.length()));
return Math.max(minPos, targetPos);
}
}
return this.text.length();
}
public String getSelectionBeforeText() {
2025-03-13 17:14:45 -04:00
return this.startSelection == 0 ? "" : this.text.substring(0, this.startSelection);
}
public String getSelectionAfterText() {
return this.text.substring(this.endSelection);
}
public void mouseClicked(int xMouse, int yMouse, int mouseButton) {
this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height;
2025-03-13 17:14:45 -04:00
if (this.active) {
this.startSelection = this.endSelection = this.cursorPosition = this.getSelectionPos(xMouse, yMouse);
this.clicked = mouseButton == 0;
this.doubleClicked = false;
long time = System.currentTimeMillis();
2025-03-13 17:14:45 -04:00
if (this.clicked && this.container.linesCount * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) {
this.clicked = false;
this.clickScrolling = true;
2025-03-13 17:14:45 -04:00
} else if (time - this.lastClicked < 500L) {
this.doubleClicked = true;
Matcher m = this.container.regexWord.matcher(this.text);
2025-03-13 17:14:45 -04:00
while (m.find()) {
if (this.cursorPosition > m.start() && this.cursorPosition < m.end()) {
this.startSelection = m.start();
this.endSelection = m.end();
break;
}
}
}
this.lastClicked = time;
activeTextfield = this;
}
}
public void updateCursorCounter() {
++this.cursorCounter;
}
public void setText(String text) {
if (text == null) {
return;
}
text = text.replace("\r", "");
text = text.replace("\t", " ");
2025-03-13 17:14:45 -04:00
if (this.text == null || !this.text.equals(text)) {
if (this.listener != null) {
this.listener.textUpdate(text);
}
2025-03-13 17:14:45 -04:00
if (!this.undoing) {
this.undoList.add(new GuiScriptTextArea.UndoData(this.text, this.cursorPosition));
this.redoList.clear();
}
this.text = text;
2025-12-16 18:33:55 +02:00
//this.container = new TextContainer(text);
this.container = new JavaTextContainer(text);
this.container.init(this.width, this.height);
2025-03-13 17:14:45 -04:00
if (this.enableCodeHighlighting) {
this.container.formatCodeText();
}
2025-03-13 17:14:45 -04:00
if (this.scrolledLine > this.container.linesCount - this.container.visibleLines) {
this.scrolledLine = Math.max(0, this.container.linesCount - this.container.visibleLines);
}
2025-10-15 06:28:43 -04:00
clampSelectionBounds();
}
}
public String getText() {
return this.text;
}
public boolean isEnabled() {
return this.enabled && this.visible;
}
public boolean hasVerticalScrollbar() {
return this.container.visibleLines < this.container.linesCount;
}
public void enableCodeHighlighting() {
this.enableCodeHighlighting = true;
this.container.formatCodeText();
}
public void setListener(ITextChangeListener listener) {
this.listener = listener;
}
2025-10-15 06:28:43 -04:00
private void clampSelectionBounds() {
if (this.text == null || this.text.isEmpty()) {
this.startSelection = this.endSelection = this.cursorPosition = 0;
return;
}
int length = this.text.length();
this.startSelection = Math.max(0, Math.min(this.startSelection, length));
this.endSelection = Math.max(0, Math.min(this.endSelection, length));
this.cursorPosition = Math.max(0, Math.min(this.cursorPosition, length));
}
private static class CommentToggleInfo {
public final String text;
public final int delta; // newText.length() - oldText.length()
public final int nonWs;
public CommentToggleInfo(String text, int delta, int nonWs) {
this.text = text;
this.delta = delta;
this.nonWs = nonWs;
}
}
private CommentToggleInfo toggleLine(String lineText) {
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 newLineText;
if (hasComment) {
int cut = Math.min(nonWs + 2, lineText.length());
newLineText = lineText.substring(0, nonWs) + (cut <= lineText.length() ? lineText.substring(cut) : "");
} else if (hasContent) {
newLineText = lineText.substring(0, nonWs) + "//" + lineText.substring(nonWs);
} else {
newLineText = lineText;
}
return new CommentToggleInfo(newLineText, newLineText.length() - lineText.length(), nonWs);
}
class UndoData {
public String text;
public int cursorPosition;
public UndoData(String text, int cursorPosition) {
this.text = text;
this.cursorPosition = cursorPosition;
}
}
}