feat(CustomSingleLineTextField, CustomMultiLineTextArea): add word navigation, page up/down, word delete and tab keys

This commit is contained in:
extio-de
2026-10-09 20:42:33 +02:00
parent 459d2f41ab
commit b2dfbf56cb
2 changed files with 382 additions and 80 deletions

View File

@@ -183,7 +183,7 @@ public class CustomMultiLineTextArea extends Component {
deleteSelection(); deleteSelection();
insertChar('\n'); insertChar('\n');
} }
else if (ch == '\b') { else if (ch == '\b' && !e.isControlDown()) {
backspace(); backspace();
} }
} }
@@ -196,10 +196,19 @@ public class CustomMultiLineTextArea extends Component {
final boolean shiftPressed = e.isShiftDown(); final boolean shiftPressed = e.isShiftDown();
final boolean ctrlPressed = e.isControlDown(); final boolean ctrlPressed = e.isControlDown();
final boolean altPressed = e.isAltDown();
switch (e.getKeyCode()) { switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT: case KeyEvent.VK_LEFT:
if (ctrlPressed || altPressed) {
if (CustomMultiLineTextArea.this.caretPosition > 0) { if (CustomMultiLineTextArea.this.caretPosition > 0) {
moveCaretToPreviousWordStart();
if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
}
}
}
else if (CustomMultiLineTextArea.this.caretPosition > 0) {
CustomMultiLineTextArea.this.caretPosition--; CustomMultiLineTextArea.this.caretPosition--;
if (!shiftPressed) { if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition; CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
@@ -209,7 +218,15 @@ public class CustomMultiLineTextArea extends Component {
} }
break; break;
case KeyEvent.VK_RIGHT: case KeyEvent.VK_RIGHT:
if (ctrlPressed || altPressed) {
if (CustomMultiLineTextArea.this.caretPosition < CustomMultiLineTextArea.this.text.length()) { if (CustomMultiLineTextArea.this.caretPosition < CustomMultiLineTextArea.this.text.length()) {
moveCaretToNextWordEnd();
if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
}
}
}
else if (CustomMultiLineTextArea.this.caretPosition < CustomMultiLineTextArea.this.text.length()) {
CustomMultiLineTextArea.this.caretPosition++; CustomMultiLineTextArea.this.caretPosition++;
if (!shiftPressed) { if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition; CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
@@ -230,20 +247,50 @@ public class CustomMultiLineTextArea extends Component {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition; CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
} }
break; break;
case KeyEvent.VK_PAGE_UP:
moveCaretByPage(true);
if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
}
break;
case KeyEvent.VK_PAGE_DOWN:
moveCaretByPage(false);
if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
}
break;
case KeyEvent.VK_HOME: case KeyEvent.VK_HOME:
if (ctrlPressed) {
CustomMultiLineTextArea.this.caretPosition = 0;
ensureCaretVisible();
CustomMultiLineTextArea.this.dirty = true;
}
else {
moveCaretToLineStart(); moveCaretToLineStart();
}
if (!shiftPressed) { if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition; CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
} }
break; break;
case KeyEvent.VK_END: case KeyEvent.VK_END:
if (ctrlPressed) {
CustomMultiLineTextArea.this.caretPosition = CustomMultiLineTextArea.this.text.length();
ensureCaretVisible();
CustomMultiLineTextArea.this.dirty = true;
}
else {
moveCaretToLineEnd(); moveCaretToLineEnd();
}
if (!shiftPressed) { if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition; CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
} }
break; break;
case KeyEvent.VK_DELETE: case KeyEvent.VK_DELETE:
if (!CustomMultiLineTextArea.this.readonly) { if (ctrlPressed && !CustomMultiLineTextArea.this.readonly) {
deleteWordForward();
e.consume();
}
else if (!CustomMultiLineTextArea.this.readonly) {
if (hasSelection()) { if (hasSelection()) {
if (e.isShiftDown()) { if (e.isShiftDown()) {
cut(); cut();
@@ -261,6 +308,18 @@ public class CustomMultiLineTextArea extends Component {
} }
} }
break; break;
case KeyEvent.VK_BACK_SPACE:
if (ctrlPressed && !CustomMultiLineTextArea.this.readonly) {
deleteWordBackward();
e.consume();
}
break;
case KeyEvent.VK_TAB:
if (!CustomMultiLineTextArea.this.readonly) {
insertChar('\t');
e.consume();
}
break;
case KeyEvent.VK_C: case KeyEvent.VK_C:
if (ctrlPressed) { if (ctrlPressed) {
copy(); copy();
@@ -447,6 +506,69 @@ public class CustomMultiLineTextArea extends Component {
} }
} }
private void deleteWordBackward() {
if (hasSelection()) {
deleteSelection();
return;
}
if (this.caretPosition == 0) {
return;
}
int position = this.caretPosition;
if (isWordCharacter(this.text.charAt(position - 1))) {
while (position > 0 && isWordCharacter(this.text.charAt(position - 1))) {
position--;
}
}
else {
while (position > 0 && !isWordCharacter(this.text.charAt(position - 1))) {
position--;
}
}
this.text = this.text.substring(0, position) + this.text.substring(this.caretPosition);
this.caretPosition = position;
this.selectionAnchor = position;
this.cachedWrappedLines = null;
ensureCaretVisible();
notifyTextChanged();
this.dirty = true;
}
private void deleteWordForward() {
if (hasSelection()) {
deleteSelection();
return;
}
final int length = this.text.length();
if (this.caretPosition >= length) {
return;
}
int position = this.caretPosition;
if (isWordCharacter(this.text.charAt(position))) {
while (position < length && isWordCharacter(this.text.charAt(position))) {
position++;
}
}
else {
while (position < length && !isWordCharacter(this.text.charAt(position))) {
position++;
}
}
this.text = this.text.substring(0, this.caretPosition) + this.text.substring(position);
this.cachedWrappedLines = null;
ensureCaretVisible();
notifyTextChanged();
this.dirty = true;
}
private void notifyTextChanged() { private void notifyTextChanged() {
this.recalculateScrollOffset(); this.recalculateScrollOffset();
if (this.onTextChanged != null) { if (this.onTextChanged != null) {
@@ -491,6 +613,62 @@ public class CustomMultiLineTextArea extends Component {
} }
} }
private void moveCaretByPage(final boolean up) {
final List<String> wrappedLines = getWrappedLines();
final int lineHeight = getLineHeight();
final int linesPerPage = Math.max(1, getVisibleHeight() / lineHeight);
final int currentWrappedLine = getWrappedLineIndexAtCaret();
final int lineStart = getCharOffsetAtWrappedLineStart(currentWrappedLine);
final int offsetInLine = this.caretPosition - lineStart;
final int targetWrappedLine = Math.max(0, Math.min(wrappedLines.size() - 1, currentWrappedLine + (up ? -linesPerPage : linesPerPage)));
if (targetWrappedLine != currentWrappedLine) {
final int targetLineStart = getCharOffsetAtWrappedLineStart(targetWrappedLine);
final int targetLineLength = wrappedLines.get(targetWrappedLine).length();
this.caretPosition = targetLineStart + Math.min(offsetInLine, targetLineLength);
ensureCaretVisible();
this.dirty = true;
}
}
private void moveCaretToPreviousWordStart() {
int position = this.caretPosition;
while (position > 0 && !isWordCharacter(this.text.charAt(position - 1))) {
position--;
}
while (position > 0 && isWordCharacter(this.text.charAt(position - 1))) {
position--;
}
this.caretPosition = position;
ensureCaretVisible();
this.dirty = true;
}
private void moveCaretToNextWordEnd() {
int position = this.caretPosition;
final int length = this.text.length();
if (position < length && isWordCharacter(this.text.charAt(position))) {
while (position < length && isWordCharacter(this.text.charAt(position))) {
position++;
}
}
else {
while (position < length && !isWordCharacter(this.text.charAt(position))) {
position++;
}
}
this.caretPosition = position;
ensureCaretVisible();
this.dirty = true;
}
private void moveCaretToLineStart() { private void moveCaretToLineStart() {
final int currentWrappedLine = getWrappedLineIndexAtCaret(); final int currentWrappedLine = getWrappedLineIndexAtCaret();
this.caretPosition = getCharOffsetAtWrappedLineStart(currentWrappedLine); this.caretPosition = getCharOffsetAtWrappedLineStart(currentWrappedLine);

View File

@@ -142,7 +142,7 @@ public class CustomSingleLineTextField extends Component {
deleteSelection(); deleteSelection();
insertChar(ch); insertChar(ch);
} }
else if (ch == '\b') { else if (ch == '\b' && !e.isControlDown()) {
backspace(); backspace();
} }
} }
@@ -155,6 +155,7 @@ public class CustomSingleLineTextField extends Component {
final boolean shiftPressed = e.isShiftDown(); final boolean shiftPressed = e.isShiftDown();
final boolean ctrlPressed = e.isControlDown(); final boolean ctrlPressed = e.isControlDown();
final boolean altPressed = e.isAltDown();
switch (e.getKeyCode()) { switch (e.getKeyCode()) {
case KeyEvent.VK_ENTER: case KeyEvent.VK_ENTER:
@@ -165,7 +166,15 @@ public class CustomSingleLineTextField extends Component {
} }
break; break;
case KeyEvent.VK_LEFT: case KeyEvent.VK_LEFT:
if (ctrlPressed || altPressed) {
if (CustomSingleLineTextField.this.caretPosition > 0) { if (CustomSingleLineTextField.this.caretPosition > 0) {
moveCaretToPreviousWordStart();
if (!shiftPressed) {
CustomSingleLineTextField.this.selectionAnchor = CustomSingleLineTextField.this.caretPosition;
}
}
}
else if (CustomSingleLineTextField.this.caretPosition > 0) {
CustomSingleLineTextField.this.caretPosition--; CustomSingleLineTextField.this.caretPosition--;
if (!shiftPressed) { if (!shiftPressed) {
CustomSingleLineTextField.this.selectionAnchor = CustomSingleLineTextField.this.caretPosition; CustomSingleLineTextField.this.selectionAnchor = CustomSingleLineTextField.this.caretPosition;
@@ -175,7 +184,15 @@ public class CustomSingleLineTextField extends Component {
} }
break; break;
case KeyEvent.VK_RIGHT: case KeyEvent.VK_RIGHT:
if (ctrlPressed || altPressed) {
if (CustomSingleLineTextField.this.caretPosition < CustomSingleLineTextField.this.text.length()) { if (CustomSingleLineTextField.this.caretPosition < CustomSingleLineTextField.this.text.length()) {
moveCaretToNextWordEnd();
if (!shiftPressed) {
CustomSingleLineTextField.this.selectionAnchor = CustomSingleLineTextField.this.caretPosition;
}
}
}
else if (CustomSingleLineTextField.this.caretPosition < CustomSingleLineTextField.this.text.length()) {
CustomSingleLineTextField.this.caretPosition++; CustomSingleLineTextField.this.caretPosition++;
if (!shiftPressed) { if (!shiftPressed) {
CustomSingleLineTextField.this.selectionAnchor = CustomSingleLineTextField.this.caretPosition; CustomSingleLineTextField.this.selectionAnchor = CustomSingleLineTextField.this.caretPosition;
@@ -201,7 +218,11 @@ public class CustomSingleLineTextField extends Component {
CustomSingleLineTextField.this.dirty = true; CustomSingleLineTextField.this.dirty = true;
break; break;
case KeyEvent.VK_DELETE: case KeyEvent.VK_DELETE:
if (!CustomSingleLineTextField.this.readonly) { if (ctrlPressed && !CustomSingleLineTextField.this.readonly) {
deleteWordForward();
e.consume();
}
else if (!CustomSingleLineTextField.this.readonly) {
if (hasSelection()) { if (hasSelection()) {
if (e.isShiftDown()) { if (e.isShiftDown()) {
cut(); cut();
@@ -218,6 +239,12 @@ public class CustomSingleLineTextField extends Component {
} }
} }
break; break;
case KeyEvent.VK_BACK_SPACE:
if (ctrlPressed && !CustomSingleLineTextField.this.readonly) {
deleteWordBackward();
e.consume();
}
break;
case KeyEvent.VK_C: case KeyEvent.VK_C:
if (ctrlPressed) { if (ctrlPressed) {
copy(); copy();
@@ -364,6 +391,67 @@ public class CustomSingleLineTextField extends Component {
} }
} }
private void deleteWordBackward() {
if (hasSelection()) {
deleteSelection();
return;
}
if (this.caretPosition == 0) {
return;
}
int position = this.caretPosition;
if (isWordCharacter(this.text.charAt(position - 1))) {
while (position > 0 && isWordCharacter(this.text.charAt(position - 1))) {
position--;
}
}
else {
while (position > 0 && !isWordCharacter(this.text.charAt(position - 1))) {
position--;
}
}
this.text = this.text.substring(0, position) + this.text.substring(this.caretPosition);
this.caretPosition = position;
this.selectionAnchor = position;
ensureCaretVisible();
notifyTextChanged();
this.dirty = true;
}
private void deleteWordForward() {
if (hasSelection()) {
deleteSelection();
return;
}
final int length = this.text.length();
if (this.caretPosition >= length) {
return;
}
int position = this.caretPosition;
if (isWordCharacter(this.text.charAt(position))) {
while (position < length && isWordCharacter(this.text.charAt(position))) {
position++;
}
}
else {
while (position < length && !isWordCharacter(this.text.charAt(position))) {
position++;
}
}
this.text = this.text.substring(0, this.caretPosition) + this.text.substring(position);
ensureCaretVisible();
notifyTextChanged();
this.dirty = true;
}
private void notifyTextChanged() { private void notifyTextChanged() {
if (this.onTextChanged != null) { if (this.onTextChanged != null) {
this.onTextChanged.accept(this.text); this.onTextChanged.accept(this.text);
@@ -529,6 +617,42 @@ public class CustomSingleLineTextField extends Component {
} }
} }
private void moveCaretToPreviousWordStart() {
int position = this.caretPosition;
while (position > 0 && !isWordCharacter(this.text.charAt(position - 1))) {
position--;
}
while (position > 0 && isWordCharacter(this.text.charAt(position - 1))) {
position--;
}
this.caretPosition = position;
ensureCaretVisible();
this.dirty = true;
}
private void moveCaretToNextWordEnd() {
int position = this.caretPosition;
final int length = this.text.length();
if (position < length && isWordCharacter(this.text.charAt(position))) {
while (position < length && isWordCharacter(this.text.charAt(position))) {
position++;
}
}
else {
while (position < length && !isWordCharacter(this.text.charAt(position))) {
position++;
}
}
this.caretPosition = position;
ensureCaretVisible();
this.dirty = true;
}
private boolean isWordCharacter(final char ch) { private boolean isWordCharacter(final char ch) {
return Character.isLetterOrDigit(ch) || ch == '_'; return Character.isLetterOrDigit(ch) || ch == '_';
} }