diff --git a/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomMultiLineTextArea.java b/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomMultiLineTextArea.java index 8c3b44f..559492f 100644 --- a/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomMultiLineTextArea.java +++ b/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomMultiLineTextArea.java @@ -183,7 +183,7 @@ public class CustomMultiLineTextArea extends Component { deleteSelection(); insertChar('\n'); } - else if (ch == '\b') { + else if (ch == '\b' && !e.isControlDown()) { backspace(); } } @@ -196,71 +196,130 @@ public class CustomMultiLineTextArea extends Component { final boolean shiftPressed = e.isShiftDown(); final boolean ctrlPressed = e.isControlDown(); + final boolean altPressed = e.isAltDown(); switch (e.getKeyCode()) { case KeyEvent.VK_LEFT: + if (ctrlPressed || altPressed) { if (CustomMultiLineTextArea.this.caretPosition > 0) { - CustomMultiLineTextArea.this.caretPosition--; + moveCaretToPreviousWordStart(); if (!shiftPressed) { 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()) { - CustomMultiLineTextArea.this.caretPosition++; + moveCaretToNextWordEnd(); if (!shiftPressed) { 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; } - 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_HOME: - 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; + } + 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: if (ctrlPressed) { 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() { this.recalculateScrollOffset(); if (this.onTextChanged != null) { @@ -491,6 +613,62 @@ public class CustomMultiLineTextArea extends Component { } } + private void moveCaretByPage(final boolean up) { + final List 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() { final int currentWrappedLine = getWrappedLineIndexAtCaret(); this.caretPosition = getCharOffsetAtWrappedLineStart(currentWrappedLine); diff --git a/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomSingleLineTextField.java b/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomSingleLineTextField.java index 0c8280c..c22906a 100644 --- a/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomSingleLineTextField.java +++ b/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomSingleLineTextField.java @@ -142,9 +142,9 @@ public class CustomSingleLineTextField extends Component { deleteSelection(); insertChar(ch); } - else if (ch == '\b') { - backspace(); - } + else if (ch == '\b' && !e.isControlDown()) { + backspace(); + } } @Override @@ -155,6 +155,7 @@ public class CustomSingleLineTextField extends Component { final boolean shiftPressed = e.isShiftDown(); final boolean ctrlPressed = e.isControlDown(); + final boolean altPressed = e.isAltDown(); switch (e.getKeyCode()) { case KeyEvent.VK_ENTER: @@ -164,26 +165,42 @@ public class CustomSingleLineTextField extends Component { } } break; - case KeyEvent.VK_LEFT: + case KeyEvent.VK_LEFT: + if (ctrlPressed || altPressed) { if (CustomSingleLineTextField.this.caretPosition > 0) { - CustomSingleLineTextField.this.caretPosition--; + moveCaretToPreviousWordStart(); if (!shiftPressed) { 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()) { - CustomSingleLineTextField.this.caretPosition++; + moveCaretToNextWordEnd(); if (!shiftPressed) { 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: CustomSingleLineTextField.this.caretPosition = 0; if (!shiftPressed) { @@ -200,24 +217,34 @@ public class CustomSingleLineTextField extends Component { ensureCaretVisible(); CustomSingleLineTextField.this.dirty = true; break; - case KeyEvent.VK_DELETE: - if (!CustomSingleLineTextField.this.readonly) { - if (hasSelection()) { - if (e.isShiftDown()) { - cut(); - } - else { - deleteSelection(); - } + case KeyEvent.VK_DELETE: + if (ctrlPressed && !CustomSingleLineTextField.this.readonly) { + deleteWordForward(); + e.consume(); + } + else if (!CustomSingleLineTextField.this.readonly) { + if (hasSelection()) { + if (e.isShiftDown()) { + cut(); } - 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; + else { + deleteSelection(); } } - 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: if (ctrlPressed) { 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() { if (this.onTextChanged != null) { 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) { return Character.isLetterOrDigit(ch) || ch == '_'; }