feat(CustomSingleLineTextField, CustomMultiLineTextArea): add word navigation, page up/down, word delete and tab keys
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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 == '_';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user