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,71 +196,130 @@ 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) {
CustomMultiLineTextArea.this.caretPosition--; moveCaretToPreviousWordStart();
if (!shiftPressed) { if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition; CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
} }
ensureCaretVisible();
CustomMultiLineTextArea.this.dirty = true;
} }
break; }
case KeyEvent.VK_RIGHT: else if (CustomMultiLineTextArea.this.caretPosition > 0) {
CustomMultiLineTextArea.this.caretPosition--;
if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
}
ensureCaretVisible();
CustomMultiLineTextArea.this.dirty = true;
}
break;
case KeyEvent.VK_RIGHT:
if (ctrlPressed || altPressed) {
if (CustomMultiLineTextArea.this.caretPosition < CustomMultiLineTextArea.this.text.length()) { if (CustomMultiLineTextArea.this.caretPosition < CustomMultiLineTextArea.this.text.length()) {
CustomMultiLineTextArea.this.caretPosition++; moveCaretToNextWordEnd();
if (!shiftPressed) { if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition; CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
} }
ensureCaretVisible(); }
}
else if (CustomMultiLineTextArea.this.caretPosition < CustomMultiLineTextArea.this.text.length()) {
CustomMultiLineTextArea.this.caretPosition++;
if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
}
ensureCaretVisible();
CustomMultiLineTextArea.this.dirty = true;
}
break;
case KeyEvent.VK_UP:
moveCaretUp();
if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
}
break;
case KeyEvent.VK_DOWN:
moveCaretDown();
if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
}
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:
if (ctrlPressed) {
CustomMultiLineTextArea.this.caretPosition = 0;
ensureCaretVisible();
CustomMultiLineTextArea.this.dirty = true;
}
else {
moveCaretToLineStart();
}
if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
}
break;
case KeyEvent.VK_END:
if (ctrlPressed) {
CustomMultiLineTextArea.this.caretPosition = CustomMultiLineTextArea.this.text.length();
ensureCaretVisible();
CustomMultiLineTextArea.this.dirty = true;
}
else {
moveCaretToLineEnd();
}
if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
}
break;
case KeyEvent.VK_DELETE:
if (ctrlPressed && !CustomMultiLineTextArea.this.readonly) {
deleteWordForward();
e.consume();
}
else if (!CustomMultiLineTextArea.this.readonly) {
if (hasSelection()) {
if (e.isShiftDown()) {
cut();
}
else {
deleteSelection();
}
}
else if (CustomMultiLineTextArea.this.caretPosition < CustomMultiLineTextArea.this.text.length()) {
CustomMultiLineTextArea.this.text = CustomMultiLineTextArea.this.text.substring(0, CustomMultiLineTextArea.this.caretPosition) +
CustomMultiLineTextArea.this.text.substring(CustomMultiLineTextArea.this.caretPosition + 1);
CustomMultiLineTextArea.this.cachedWrappedLines = null;
notifyTextChanged();
CustomMultiLineTextArea.this.dirty = true; CustomMultiLineTextArea.this.dirty = true;
} }
break; }
case KeyEvent.VK_UP: break;
moveCaretUp(); case KeyEvent.VK_BACK_SPACE:
if (!shiftPressed) { if (ctrlPressed && !CustomMultiLineTextArea.this.readonly) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition; deleteWordBackward();
} e.consume();
break; }
case KeyEvent.VK_DOWN: break;
moveCaretDown(); case KeyEvent.VK_TAB:
if (!shiftPressed) { if (!CustomMultiLineTextArea.this.readonly) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition; insertChar('\t');
} e.consume();
break; }
case KeyEvent.VK_HOME: break;
moveCaretToLineStart();
if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
}
break;
case KeyEvent.VK_END:
moveCaretToLineEnd();
if (!shiftPressed) {
CustomMultiLineTextArea.this.selectionAnchor = CustomMultiLineTextArea.this.caretPosition;
}
break;
case KeyEvent.VK_DELETE:
if (!CustomMultiLineTextArea.this.readonly) {
if (hasSelection()) {
if (e.isShiftDown()) {
cut();
}
else {
deleteSelection();
}
}
else if (CustomMultiLineTextArea.this.caretPosition < CustomMultiLineTextArea.this.text.length()) {
CustomMultiLineTextArea.this.text = CustomMultiLineTextArea.this.text.substring(0, CustomMultiLineTextArea.this.caretPosition) +
CustomMultiLineTextArea.this.text.substring(CustomMultiLineTextArea.this.caretPosition + 1);
CustomMultiLineTextArea.this.cachedWrappedLines = null;
notifyTextChanged();
CustomMultiLineTextArea.this.dirty = true;
}
}
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,9 +142,9 @@ public class CustomSingleLineTextField extends Component {
deleteSelection(); deleteSelection();
insertChar(ch); insertChar(ch);
} }
else if (ch == '\b') { else if (ch == '\b' && !e.isControlDown()) {
backspace(); backspace();
} }
} }
@Override @Override
@@ -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:
@@ -164,26 +165,42 @@ 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) {
CustomSingleLineTextField.this.caretPosition--; moveCaretToPreviousWordStart();
if (!shiftPressed) { if (!shiftPressed) {
CustomSingleLineTextField.this.selectionAnchor = CustomSingleLineTextField.this.caretPosition; CustomSingleLineTextField.this.selectionAnchor = CustomSingleLineTextField.this.caretPosition;
} }
ensureCaretVisible();
CustomSingleLineTextField.this.dirty = true;
} }
break; }
case KeyEvent.VK_RIGHT: else if (CustomSingleLineTextField.this.caretPosition > 0) {
CustomSingleLineTextField.this.caretPosition--;
if (!shiftPressed) {
CustomSingleLineTextField.this.selectionAnchor = CustomSingleLineTextField.this.caretPosition;
}
ensureCaretVisible();
CustomSingleLineTextField.this.dirty = true;
}
break;
case KeyEvent.VK_RIGHT:
if (ctrlPressed || altPressed) {
if (CustomSingleLineTextField.this.caretPosition < CustomSingleLineTextField.this.text.length()) { if (CustomSingleLineTextField.this.caretPosition < CustomSingleLineTextField.this.text.length()) {
CustomSingleLineTextField.this.caretPosition++; moveCaretToNextWordEnd();
if (!shiftPressed) { if (!shiftPressed) {
CustomSingleLineTextField.this.selectionAnchor = CustomSingleLineTextField.this.caretPosition; CustomSingleLineTextField.this.selectionAnchor = CustomSingleLineTextField.this.caretPosition;
} }
ensureCaretVisible();
CustomSingleLineTextField.this.dirty = true;
} }
break; }
else if (CustomSingleLineTextField.this.caretPosition < CustomSingleLineTextField.this.text.length()) {
CustomSingleLineTextField.this.caretPosition++;
if (!shiftPressed) {
CustomSingleLineTextField.this.selectionAnchor = CustomSingleLineTextField.this.caretPosition;
}
ensureCaretVisible();
CustomSingleLineTextField.this.dirty = true;
}
break;
case KeyEvent.VK_HOME: case KeyEvent.VK_HOME:
CustomSingleLineTextField.this.caretPosition = 0; CustomSingleLineTextField.this.caretPosition = 0;
if (!shiftPressed) { if (!shiftPressed) {
@@ -200,24 +217,34 @@ public class CustomSingleLineTextField extends Component {
ensureCaretVisible(); ensureCaretVisible();
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) {
if (hasSelection()) { deleteWordForward();
if (e.isShiftDown()) { e.consume();
cut(); }
} else if (!CustomSingleLineTextField.this.readonly) {
else { if (hasSelection()) {
deleteSelection(); if (e.isShiftDown()) {
} cut();
} }
else if (CustomSingleLineTextField.this.caretPosition < CustomSingleLineTextField.this.text.length()) { else {
CustomSingleLineTextField.this.text = CustomSingleLineTextField.this.text.substring(0, CustomSingleLineTextField.this.caretPosition) + deleteSelection();
CustomSingleLineTextField.this.text.substring(CustomSingleLineTextField.this.caretPosition + 1);
notifyTextChanged();
CustomSingleLineTextField.this.dirty = true;
} }
} }
break; else if (CustomSingleLineTextField.this.caretPosition < CustomSingleLineTextField.this.text.length()) {
CustomSingleLineTextField.this.text = CustomSingleLineTextField.this.text.substring(0, CustomSingleLineTextField.this.caretPosition) +
CustomSingleLineTextField.this.text.substring(CustomSingleLineTextField.this.caretPosition + 1);
notifyTextChanged();
CustomSingleLineTextField.this.dirty = true;
}
}
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 == '_';
} }