com.kotcrab.vis.ui.widget.VisTextField Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vis-ui Show documentation
Show all versions of vis-ui Show documentation
UI toolkit and flat design skin for scene2d.ui
/*
* Copyright 2014-2017 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kotcrab.vis.ui.widget;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Cursor.SystemCursor;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.BitmapFont.BitmapFontData;
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
import com.badlogic.gdx.graphics.g2d.GlyphLayout.GlyphRun;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.*;
import com.badlogic.gdx.scenes.scene2d.ui.TextField;
import com.badlogic.gdx.scenes.scene2d.ui.TextField.DefaultOnscreenKeyboard;
import com.badlogic.gdx.scenes.scene2d.ui.TextField.OnscreenKeyboard;
import com.badlogic.gdx.scenes.scene2d.ui.TextField.TextFieldStyle;
import com.badlogic.gdx.scenes.scene2d.ui.Widget;
import com.badlogic.gdx.scenes.scene2d.ui.Window;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.scenes.scene2d.utils.Disableable;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.badlogic.gdx.scenes.scene2d.utils.UIUtils;
import com.badlogic.gdx.utils.*;
import com.badlogic.gdx.utils.Timer.Task;
import com.kotcrab.vis.ui.FocusManager;
import com.kotcrab.vis.ui.Focusable;
import com.kotcrab.vis.ui.VisUI;
import com.kotcrab.vis.ui.util.BorderOwner;
import com.kotcrab.vis.ui.util.CursorManager;
import java.lang.StringBuilder;
/**
* Extends functionality of standard {@link TextField}. Style supports over, and focus border. Improved text input.
* Due to scope of changes made this widget is not compatible with {@link TextField}.
* @author mzechner
* @author Nathan Sweet
* @author Kotcrab
* @see TextField
*/
public class VisTextField extends Widget implements Disableable, Focusable, BorderOwner {
static private final char BACKSPACE = 8;
static protected final char ENTER_DESKTOP = '\r';
static protected final char ENTER_ANDROID = '\n';
static private final char TAB = '\t';
static private final char DELETE = 127;
static private final char BULLET = 8226;
static private final Vector2 tmp1 = new Vector2();
static private final Vector2 tmp2 = new Vector2();
static private final Vector2 tmp3 = new Vector2();
static public float keyRepeatInitialTime = 0.4f;
/** Repeat times for keys handled by {@link InputListener#keyDown(InputEvent, int)} such as navigation arrows */
static public float keyRepeatTime = 0.04f;
protected String text;
protected int cursor, selectionStart;
protected boolean hasSelection;
protected boolean writeEnters;
protected final GlyphLayout layout = new GlyphLayout();
protected final FloatArray glyphPositions = new FloatArray();
private String messageText;
protected CharSequence displayText;
Clipboard clipboard;
InputListener inputListener;
TextFieldListener listener;
TextFieldFilter filter;
OnscreenKeyboard keyboard = new DefaultOnscreenKeyboard();
boolean focusTraversal = true, onlyFontChars = true, disabled;
boolean enterKeyFocusTraversal = false;
private int textHAlign = Align.left;
private float selectionX, selectionWidth;
String undoText = "";
int undoCursorPos = 0;
long lastChangeTime;
boolean passwordMode;
private StringBuilder passwordBuffer;
private char passwordCharacter = BULLET;
protected float fontOffset, textHeight, textOffset;
float renderOffset;
private int visibleTextStart, visibleTextEnd;
private int maxLength = 0;
private float blinkTime = 0.45f;
boolean cursorOn = true;
long lastBlink;
KeyRepeatTask keyRepeatTask = new KeyRepeatTask();
boolean programmaticChangeEvents;
// vis fields
VisTextFieldStyle style;
private ClickListener clickListener;
private boolean drawBorder;
private boolean focusBorderEnabled = true;
private boolean inputValid = true;
private boolean ignoreEqualsTextChange = true;
private boolean readOnly = false;
private float cursorPercentHeight = 0.8f;
public VisTextField () {
this("", VisUI.getSkin().get(VisTextFieldStyle.class));
}
public VisTextField (String text) {
this(text, VisUI.getSkin().get(VisTextFieldStyle.class));
}
public VisTextField (String text, String styleName) {
this(text, VisUI.getSkin().get(styleName, VisTextFieldStyle.class));
}
public VisTextField (String text, VisTextFieldStyle style) {
setStyle(style);
clipboard = Gdx.app.getClipboard();
initialize();
setText(text);
setSize(getPrefWidth(), getPrefHeight());
}
protected void initialize () {
addListener(inputListener = createInputListener());
addListener(clickListener = new ClickListener() {
@Override
public void enter (InputEvent event, float x, float y, int pointer, Actor fromActor) {
super.enter(event, x, y, pointer, fromActor);
if (pointer == -1 && isDisabled() == false) {
Gdx.graphics.setSystemCursor(SystemCursor.Ibeam);
}
}
@Override
public void exit (InputEvent event, float x, float y, int pointer, Actor toActor) {
super.exit(event, x, y, pointer, toActor);
if (pointer == -1) {
CursorManager.restoreDefaultCursor();
}
}
});
}
protected InputListener createInputListener () {
return new TextFieldClickListener();
}
protected int letterUnderCursor (float x) {
x -= textOffset + fontOffset - style.font.getData().cursorX - glyphPositions.get(visibleTextStart);
int n = this.glyphPositions.size;
float[] glyphPositions = this.glyphPositions.items;
for (int i = 1; i < n; i++) {
if (glyphPositions[i] > x) {
if (glyphPositions[i] - x <= x - glyphPositions[i - 1]) return i;
return i - 1;
}
}
return n - 1;
}
protected boolean isWordCharacter (char c) {
return Character.isLetterOrDigit(c);
}
protected int[] wordUnderCursor (int at) {
String text = this.text;
int start = Math.min(text.length(), at), right = text.length(), left = 0, index = start;
for (; index < right; index++) {
if (!isWordCharacter(text.charAt(index))) {
right = index;
break;
}
}
for (index = start - 1; index > -1; index--) {
if (!isWordCharacter(text.charAt(index))) {
left = index + 1;
break;
}
}
return new int[]{left, right};
}
int[] wordUnderCursor (float x) {
return wordUnderCursor(letterUnderCursor(x));
}
boolean withinMaxLength (int size) {
return maxLength <= 0 || size < maxLength;
}
public int getMaxLength () {
return this.maxLength;
}
public void setMaxLength (int maxLength) {
this.maxLength = maxLength;
}
/**
* When false, text set by {@link #setText(String)} may contain characters not in the font, a space will be displayed instead.
* When true (the default), characters not in the font are stripped by setText. Characters not in the font are always stripped
* when typed or pasted.
*/
public void setOnlyFontChars (boolean onlyFontChars) {
this.onlyFontChars = onlyFontChars;
}
/**
* Returns the text field's style. Modifying the returned style may not have an effect until
* {@link #setStyle(VisTextFieldStyle)} is called.
*/
public VisTextFieldStyle getStyle () {
return style;
}
public void setStyle (VisTextFieldStyle style) {
if (style == null) throw new IllegalArgumentException("style cannot be null.");
this.style = style;
textHeight = style.font.getCapHeight() - style.font.getDescent() * 2;
invalidateHierarchy();
}
@Override
public String toString () {
return getText();
}
protected void calculateOffsets () {
float visibleWidth = getWidth();
if (style.background != null)
visibleWidth -= style.background.getLeftWidth() + style.background.getRightWidth();
int glyphCount = glyphPositions.size;
float[] glyphPositions = this.glyphPositions.items;
// Check if the cursor has gone out the left or right side of the visible area and adjust renderOffset.
float distance = glyphPositions[Math.max(0, cursor - 1)] + renderOffset;
if (distance <= 0)
renderOffset -= distance;
else {
int index = Math.min(glyphCount - 1, cursor + 1);
float minX = glyphPositions[index] - visibleWidth;
if (-renderOffset < minX) renderOffset = -minX;
}
// Prevent renderOffset from starting too close to the end, eg after text was deleted.
float maxOffset = 0;
float width = glyphPositions[glyphCount - 1];
for (int i = glyphCount - 2; i >= 0; i--) {
float x = glyphPositions[i];
if (width - x > visibleWidth) break;
maxOffset = x;
}
if (-renderOffset > maxOffset) renderOffset = -maxOffset;
// calculate first visible char based on render offset
visibleTextStart = 0;
float startX = 0;
for (int i = 0; i < glyphCount; i++) {
if (glyphPositions[i] >= -renderOffset) {
visibleTextStart = Math.max(0, i);
startX = glyphPositions[i];
break;
}
}
// calculate last visible char based on visible width and render offset
int length = Math.min(displayText.length(), glyphPositions.length - 1);
visibleTextEnd = Math.min(length, cursor + 1);
for (; visibleTextEnd <= length; visibleTextEnd++)
if (glyphPositions[visibleTextEnd] > startX + visibleWidth) break;
visibleTextEnd = Math.max(0, visibleTextEnd - 1);
if ((textHAlign & Align.left) == 0) {
textOffset = visibleWidth - (glyphPositions[visibleTextEnd] - startX);
if ((textHAlign & Align.center) != 0) textOffset = Math.round(textOffset * 0.5f);
} else
textOffset = startX + renderOffset;
// calculate selection x position and width
if (hasSelection) {
int minIndex = Math.min(cursor, selectionStart);
int maxIndex = Math.max(cursor, selectionStart);
float minX = Math.max(glyphPositions[minIndex] - glyphPositions[visibleTextStart], -textOffset);
float maxX = Math.min(glyphPositions[maxIndex] - glyphPositions[visibleTextStart], visibleWidth - textOffset);
selectionX = minX;
selectionWidth = maxX - minX - style.font.getData().cursorX;
}
}
@Override
public void draw (Batch batch, float parentAlpha) {
Stage stage = getStage();
boolean focused = stage != null && stage.getKeyboardFocus() == this;
if (!focused) keyRepeatTask.cancel();
final BitmapFont font = style.font;
final Color fontColor = (disabled && style.disabledFontColor != null) ? style.disabledFontColor
: ((focused && style.focusedFontColor != null) ? style.focusedFontColor : style.fontColor);
final Drawable selection = style.selection;
final Drawable cursorPatch = style.cursor;
Drawable background = (disabled && style.disabledBackground != null) ? style.disabledBackground
: ((focused && style.focusedBackground != null) ? style.focusedBackground : style.background);
// vis
if (!disabled && style.backgroundOver != null && (clickListener.isOver() || focused)) {
background = style.backgroundOver;
}
Color color = getColor();
float x = getX();
float y = getY();
float width = getWidth();
float height = getHeight();
batch.setColor(color.r, color.g, color.b, color.a * parentAlpha);
float bgLeftWidth = 0, bgRightWidth = 0;
if (background != null) {
background.draw(batch, x, y, width, height);
bgLeftWidth = background.getLeftWidth();
bgRightWidth = background.getRightWidth();
}
float textY = getTextY(font, background);
calculateOffsets();
if (focused && hasSelection && selection != null) {
drawSelection(selection, batch, font, x + bgLeftWidth, y + textY);
}
float yOffset = font.isFlipped() ? -textHeight : 0;
if (displayText.length() == 0) {
if (!focused && messageText != null) {
if (style.messageFontColor != null) {
font.setColor(style.messageFontColor.r, style.messageFontColor.g, style.messageFontColor.b,
style.messageFontColor.a * color.a * parentAlpha);
} else
font.setColor(0.7f, 0.7f, 0.7f, color.a * parentAlpha);
BitmapFont messageFont = style.messageFont != null ? style.messageFont : font;
messageFont.draw(batch, messageText, x + bgLeftWidth, y + textY + yOffset, 0, messageText.length(),
width - bgLeftWidth - bgRightWidth, textHAlign, false, "...");
}
} else {
font.setColor(fontColor.r, fontColor.g, fontColor.b, fontColor.a * color.a * parentAlpha);
drawText(batch, font, x + bgLeftWidth, y + textY + yOffset);
}
if (drawBorder && focused && !disabled) {
blink();
if (cursorOn && cursorPatch != null) {
drawCursor(cursorPatch, batch, font, x + bgLeftWidth, y + textY);
}
}
// vis
if (isDisabled() == false && inputValid == false && style.errorBorder != null)
style.errorBorder.draw(batch, getX(), getY(), getWidth(), getHeight());
else if (focusBorderEnabled && drawBorder && style.focusBorder != null)
style.focusBorder.draw(batch, getX(), getY(), getWidth(), getHeight());
}
protected float getTextY (BitmapFont font, Drawable background) {
float height = getHeight();
float textY = textHeight / 2 + font.getDescent();
if (background != null) {
float bottom = background.getBottomHeight();
textY = textY + (height - background.getTopHeight() - bottom) / 2 + bottom;
} else {
textY = textY + height / 2;
}
if (font.usesIntegerPositions()) textY = (int) textY;
return textY;
}
/** Draws selection rectangle **/
protected void drawSelection (Drawable selection, Batch batch, BitmapFont font, float x, float y) {
selection.draw(batch, x + selectionX + textOffset + fontOffset, y - textHeight - font.getDescent(), selectionWidth,
textHeight);
}
protected void drawText (Batch batch, BitmapFont font, float x, float y) {
font.draw(batch, displayText, x + textOffset, y, visibleTextStart, visibleTextEnd, 0, Align.left, false);
}
protected void drawCursor (Drawable cursorPatch, Batch batch, BitmapFont font, float x, float y) {
float cursorHeight = textHeight * cursorPercentHeight;
float cursorYPadding = (textHeight - cursorHeight) / 2;
cursorPatch.draw(batch,
x + textOffset + glyphPositions.get(cursor) - glyphPositions.get(visibleTextStart) + fontOffset + font.getData().cursorX,
y - textHeight - font.getDescent() + cursorYPadding, cursorPatch.getMinWidth(), cursorHeight);
}
void updateDisplayText () {
BitmapFont font = style.font;
BitmapFontData data = font.getData();
String text = this.text;
int textLength = text.length();
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < textLength; i++) {
char c = text.charAt(i);
buffer.append(data.hasGlyph(c) ? c : ' ');
}
String newDisplayText = buffer.toString();
if (passwordMode && data.hasGlyph(passwordCharacter)) {
if (passwordBuffer == null) passwordBuffer = new StringBuilder(newDisplayText.length());
if (passwordBuffer.length() > textLength)
passwordBuffer.setLength(textLength);
else {
for (int i = passwordBuffer.length(); i < textLength; i++)
passwordBuffer.append(passwordCharacter);
}
displayText = passwordBuffer;
} else
displayText = newDisplayText;
layout.setText(font, displayText.toString().replace('\r', ' ').replace('\n', ' '));
glyphPositions.clear();
float x = 0;
if (layout.runs.size > 0) {
GlyphRun run = layout.runs.first();
fontOffset = run.xAdvances.first();
for (GlyphRun glyphRun : layout.runs) {
FloatArray xAdvances = glyphRun.xAdvances;
for (int i = 1, n = xAdvances.size; i < n; i++) {
glyphPositions.add(x);
x += xAdvances.get(i);
}
glyphPositions.add(x);
}
} else {
fontOffset = 0;
}
glyphPositions.add(x);
visibleTextStart = Math.min(visibleTextStart, glyphPositions.size);
visibleTextEnd = MathUtils.clamp(visibleTextEnd, visibleTextStart, glyphPositions.size);
if (selectionStart > newDisplayText.length()) selectionStart = textLength;
}
private void blink () {
if (!Gdx.graphics.isContinuousRendering()) {
cursorOn = true;
return;
}
long time = TimeUtils.nanoTime();
if ((time - lastBlink) / 1000000000.0f > blinkTime) {
cursorOn = !cursorOn;
lastBlink = time;
}
}
/** Copies the contents of this TextField to the {@link Clipboard} implementation set on this TextField. */
public void copy () {
if (hasSelection && !passwordMode) {
int beginIndex = Math.min(cursor, selectionStart);
int endIndex = Math.max(cursor, selectionStart);
clipboard.setContents(text.substring(Math.max(0, beginIndex), Math.min(text.length(), endIndex)));
}
}
/**
* Copies the selected contents of this TextField to the {@link Clipboard} implementation set on this TextField, then removes
* it.
*/
public void cut () {
cut(programmaticChangeEvents);
}
void cut (boolean fireChangeEvent) {
if (hasSelection && !passwordMode) {
copy();
cursor = delete(fireChangeEvent);
updateDisplayText();
}
}
void paste (String content, boolean fireChangeEvent) {
if (content == null) return;
StringBuilder buffer = new StringBuilder();
int textLength = text.length();
if (hasSelection) textLength -= Math.abs(cursor - selectionStart);
BitmapFontData data = style.font.getData();
for (int i = 0, n = content.length(); i < n; i++) {
if (!withinMaxLength(textLength + buffer.length())) break;
char c = content.charAt(i);
if (!(writeEnters && (c == ENTER_ANDROID || c == ENTER_DESKTOP))) {
if (c == '\r' || c == '\n') continue;
if (onlyFontChars && !data.hasGlyph(c)) continue;
if (filter != null && !filter.acceptChar(this, c)) continue;
}
buffer.append(c);
}
content = buffer.toString();
if (hasSelection) cursor = delete(fireChangeEvent);
if (fireChangeEvent)
changeText(text, insert(cursor, content, text));
else
text = insert(cursor, content, text);
updateDisplayText();
cursor += content.length();
}
String insert (int position, CharSequence text, String to) {
if (to.length() == 0) return text.toString();
return to.substring(0, position) + text + to.substring(position, to.length());
}
int delete (boolean fireChangeEvent) {
int from = selectionStart;
int to = cursor;
int minIndex = Math.min(from, to);
int maxIndex = Math.max(from, to);
String newText = (minIndex > 0 ? text.substring(0, minIndex) : "")
+ (maxIndex < text.length() ? text.substring(maxIndex, text.length()) : "");
if (fireChangeEvent)
changeText(text, newText);
else
text = newText;
clearSelection();
return minIndex;
}
/**
* Focuses the next TextField. If none is found, the keyboard is hidden. Does nothing if the text field is not in a stage.
* @param up If true, the TextField with the same or next smallest y coordinate is found, else the next highest.
*/
public void next (boolean up) {
Stage stage = getStage();
if (stage == null) return;
getParent().localToStageCoordinates(tmp1.set(getX(), getY()));
VisTextField textField = findNextTextField(stage.getActors(), null, tmp2, tmp1, up);
if (textField == null) { // Try to wrap around.
if (up)
tmp1.set(Float.MIN_VALUE, Float.MIN_VALUE);
else
tmp1.set(Float.MAX_VALUE, Float.MAX_VALUE);
textField = findNextTextField(getStage().getActors(), null, tmp2, tmp1, up);
}
if (textField != null) {
textField.focusField();
textField.setCursorPosition(textField.getText().length());
} else
keyboard.show(false);
}
private VisTextField findNextTextField (Array actors, VisTextField best, Vector2 bestCoords, Vector2 currentCoords, boolean up) {
Window modalWindow = findModalWindow(this);
for (int i = 0, n = actors.size; i < n; i++) {
Actor actor = actors.get(i);
if (actor == this) continue;
if (actor instanceof VisTextField) {
VisTextField textField = (VisTextField) actor;
if (modalWindow != null) {
Window nextFieldModalWindow = findModalWindow(textField);
if (nextFieldModalWindow != modalWindow) continue;
}
if (textField.isDisabled() || !textField.focusTraversal || isActorVisibleInStage(textField) == false)
continue;
Vector2 actorCoords = actor.getParent().localToStageCoordinates(tmp3.set(actor.getX(), actor.getY()));
if ((actorCoords.y < currentCoords.y || (actorCoords.y == currentCoords.y && actorCoords.x > currentCoords.x)) ^ up) {
if (best == null
|| (actorCoords.y > bestCoords.y || (actorCoords.y == bestCoords.y && actorCoords.x < bestCoords.x)) ^ up) {
best = (VisTextField) actor;
bestCoords.set(actorCoords);
}
}
} else if (actor instanceof Group)
best = findNextTextField(((Group) actor).getChildren(), best, bestCoords, currentCoords, up);
}
return best;
}
/**
* Checks if actor is visible in stage acknowledging parent visibility.
* If any parent returns false from isVisible then this method return false.
* True is returned when this actor and all its parent are visible.
*/
private boolean isActorVisibleInStage (Actor actor) {
if (actor == null) return true;
if (actor.isVisible() == false) return false;
return isActorVisibleInStage(actor.getParent());
}
private Window findModalWindow (Actor actor) {
if (actor == null) return null;
if (actor instanceof Window && ((Window) actor).isModal()) return (Window) actor;
return findModalWindow(actor.getParent());
}
public InputListener getDefaultInputListener () {
return inputListener;
}
/** @param listener May be null. */
public void setTextFieldListener (TextFieldListener listener) {
this.listener = listener;
}
/** @param filter May be null. */
public void setTextFieldFilter (TextFieldFilter filter) {
this.filter = filter;
}
public TextFieldFilter getTextFieldFilter () {
return filter;
}
/** If true (the default), tab/shift+tab will move to the next text field. */
public void setFocusTraversal (boolean focusTraversal) {
this.focusTraversal = focusTraversal;
}
/**
* If true, enter will move to the next text field with has focus traversal enabled.
* False by default. Note that to enable or disable focus traversal completely you must
* use {@link #setFocusTraversal(boolean)}
*/
public void setEnterKeyFocusTraversal (boolean enterKeyFocusTraversal) {
this.enterKeyFocusTraversal = enterKeyFocusTraversal;
}
/** @return May be null. */
public String getMessageText () {
return messageText;
}
/**
* Sets the text that will be drawn in the text field if no text has been entered.
* @param messageText may be null.
*/
public void setMessageText (String messageText) {
this.messageText = messageText;
}
/** @param str If null, "" is used. */
public void appendText (String str) {
if (str == null) str = "";
clearSelection();
cursor = text.length();
paste(str, programmaticChangeEvents);
}
/** @param str If null, "" is used. */
public void setText (String str) {
if (str == null) str = "";
if (ignoreEqualsTextChange && str.equals(text)) return;
clearSelection();
String oldText = text;
text = "";
paste(str, false);
if (programmaticChangeEvents) changeText(oldText, text);
cursor = 0;
}
/** @return Never null, might be an empty string. */
public String getText () {
return text;
}
/**
* @param oldText May be null.
* @return True if the text was changed.
*/
boolean changeText (String oldText, String newText) {
if (ignoreEqualsTextChange && newText.equals(oldText)) return false;
text = newText;
beforeChangeEventFired();
ChangeEvent changeEvent = Pools.obtain(ChangeEvent.class);
boolean cancelled = fire(changeEvent);
text = cancelled ? oldText : newText;
Pools.free(changeEvent);
return !cancelled;
}
void beforeChangeEventFired () {
}
public boolean getProgrammaticChangeEvents () {
return programmaticChangeEvents;
}
/**
* If false, methods that change the text will not fire {@link ChangeEvent}, the event will be fired only when user changes
* the text.
*/
public void setProgrammaticChangeEvents (boolean programmaticChangeEvents) {
this.programmaticChangeEvents = programmaticChangeEvents;
}
public int getSelectionStart () {
return selectionStart;
}
public String getSelection () {
return hasSelection ? text.substring(Math.min(selectionStart, cursor), Math.max(selectionStart, cursor)) : "";
}
public boolean isTextSelected () {
return hasSelection;
}
/** Sets the selected text. */
public void setSelection (int selectionStart, int selectionEnd) {
if (selectionStart < 0) throw new IllegalArgumentException("selectionStart must be >= 0");
if (selectionEnd < 0) throw new IllegalArgumentException("selectionEnd must be >= 0");
selectionStart = Math.min(text.length(), selectionStart);
selectionEnd = Math.min(text.length(), selectionEnd);
if (selectionEnd == selectionStart) {
clearSelection();
return;
}
if (selectionEnd < selectionStart) {
int temp = selectionEnd;
selectionEnd = selectionStart;
selectionStart = temp;
}
hasSelection = true;
this.selectionStart = selectionStart;
cursor = selectionEnd;
}
public void selectAll () {
setSelection(0, text.length());
}
public void clearSelection () {
hasSelection = false;
}
/** Clears VisTextField text. If programmatic change events are disabled then this will not fire change event. */
public void clearText () {
setText("");
}
/** Sets the cursor position and clears any selection. */
public void setCursorPosition (int cursorPosition) {
if (cursorPosition < 0) throw new IllegalArgumentException("cursorPosition must be >= 0");
clearSelection();
cursor = Math.min(cursorPosition, text.length());
}
public int getCursorPosition () {
return cursor;
}
public void setCursorAtTextEnd () {
setCursorPosition(0);
calculateOffsets();
setCursorPosition(getText().length());
}
/** @param cursorPercentHeight cursor size, value from 0..1 range */
public void setCursorPercentHeight (float cursorPercentHeight) {
if (cursorPercentHeight < 0 || cursorPercentHeight > 1)
throw new IllegalArgumentException("cursorPercentHeight must be >= 0 and <= 1");
this.cursorPercentHeight = cursorPercentHeight;
}
/** Default is an instance of {@link DefaultOnscreenKeyboard}. */
public OnscreenKeyboard getOnscreenKeyboard () {
return keyboard;
}
public void setOnscreenKeyboard (OnscreenKeyboard keyboard) {
this.keyboard = keyboard;
}
public void setClipboard (Clipboard clipboard) {
this.clipboard = clipboard;
}
@Override
public float getPrefWidth () {
return 150;
}
@Override
public float getPrefHeight () {
float prefHeight = textHeight;
if (style.background != null) {
prefHeight = Math.max(prefHeight + style.background.getBottomHeight() + style.background.getTopHeight(),
style.background.getMinHeight());
}
return prefHeight;
}
/**
* Sets text horizontal alignment (left, center or right).
* @see Align
*/
public void setAlignment (int alignment) {
this.textHAlign = alignment;
}
/**
* If true, the text in this text field will be shown as bullet characters.
* @see #setPasswordCharacter(char)
*/
public void setPasswordMode (boolean passwordMode) {
this.passwordMode = passwordMode;
updateDisplayText();
}
public boolean isPasswordMode () {
return passwordMode;
}
/**
* Sets the password character for the text field. The character must be present in the {@link BitmapFont}. Default is 149
* (bullet).
*/
public void setPasswordCharacter (char passwordCharacter) {
this.passwordCharacter = passwordCharacter;
if (passwordMode) updateDisplayText();
}
public void setBlinkTime (float blinkTime) {
this.blinkTime = blinkTime;
}
public boolean isDisabled () {
return disabled;
}
@Override
public void setDisabled (boolean disabled) {
this.disabled = disabled;
if (disabled) {
FocusManager.resetFocus(getStage(), this);
keyRepeatTask.cancel();
}
}
public boolean isReadOnly () {
return readOnly;
}
public void setReadOnly (boolean readOnly) {
this.readOnly = readOnly;
}
protected void moveCursor (boolean forward, boolean jump) {
int limit = forward ? text.length() : 0;
int charOffset = forward ? 0 : -1;
while ((forward ? ++cursor < limit : --cursor > limit) && jump) {
if (!continueCursor(cursor, charOffset)) break;
}
}
protected boolean continueCursor (int index, int offset) {
char c = text.charAt(index + offset);
return isWordCharacter(c);
}
/** Focuses this field, field must be added to stage before this method can be called */
public void focusField () {
if (disabled) return;
Stage stage = getStage();
FocusManager.switchFocus(stage, VisTextField.this);
setCursorPosition(0);
selectionStart = 0;
//make sure textOffset was updated, prevent issue when there was long text selected and it was changed to short text
//and field was focused. Without it textOffset would stay at max value and only one last letter will be visible in field
calculateOffsets();
if (stage != null) stage.setKeyboardFocus(VisTextField.this);
keyboard.show(true);
hasSelection = true;
}
@Override
public void focusLost () {
drawBorder = false;
}
@Override
public void focusGained () {
drawBorder = true;
}
public boolean isEmpty () {
return text.length() == 0;
}
public boolean isInputValid () {
return inputValid;
}
public void setInputValid (boolean inputValid) {
this.inputValid = inputValid;
}
@Override
public boolean isFocusBorderEnabled () {
return focusBorderEnabled;
}
@Override
public void setFocusBorderEnabled (boolean focusBorderEnabled) {
this.focusBorderEnabled = focusBorderEnabled;
}
/** @see #setIgnoreEqualsTextChange(boolean) */
public boolean isIgnoreEqualsTextChange () {
return ignoreEqualsTextChange;
}
/**
* Allows to control whether change event is sent when text field's text is changed to same same as was it before.
* Eg. current text field is 'abc' and {@link #setText(String)} is called it with 'abc' again.
* @param ignoreEqualsTextChange if true then setting text to the same as it was before will NOT fire change event.
* Default is true however it is false default {@link VisValidatableTextField} to prevent form refreshment issues -
* see issue VisEditor#165
*/
public void setIgnoreEqualsTextChange (boolean ignoreEqualsTextChange) {
this.ignoreEqualsTextChange = ignoreEqualsTextChange;
}
static public class VisTextFieldStyle extends TextFieldStyle {
public Drawable focusBorder;
public Drawable errorBorder;
public Drawable backgroundOver;
public VisTextFieldStyle () {
}
public VisTextFieldStyle (BitmapFont font, Color fontColor, Drawable cursor, Drawable selection, Drawable background) {
super(font, fontColor, cursor, selection, background);
}
public VisTextFieldStyle (VisTextFieldStyle style) {
super(style);
this.focusBorder = style.focusBorder;
this.errorBorder = style.errorBorder;
this.backgroundOver = style.backgroundOver;
}
}
/**
* Interface for listening to typed characters.
* @author mzechner
*/
static public interface TextFieldListener {
public void keyTyped (VisTextField textField, char c);
}
/**
* Interface for filtering characters entered into the text field.
* @author mzechner
*/
static public interface TextFieldFilter {
public boolean acceptChar (VisTextField textField, char c);
static public class DigitsOnlyFilter implements TextFieldFilter {
@Override
public boolean acceptChar (VisTextField textField, char c) {
return Character.isDigit(c);
}
}
}
class KeyRepeatTask extends Task {
int keycode;
@Override
public void run () {
inputListener.keyDown(null, keycode);
}
}
/** Basic input listener for the text field */
public class TextFieldClickListener extends ClickListener {
@Override
public void clicked (InputEvent event, float x, float y) {
int count = getTapCount() % 4;
if (count == 0) clearSelection();
if (count == 2) {
int[] array = wordUnderCursor(x);
setSelection(array[0], array[1]);
}
if (count == 3) selectAll();
}
@Override
public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
if (!super.touchDown(event, x, y, pointer, button)) return false;
if (pointer == 0 && button != 0) return false;
if (disabled) return true;
Stage stage = getStage();
FocusManager.switchFocus(stage, VisTextField.this);
setCursorPosition(x, y);
selectionStart = cursor;
if (stage != null) stage.setKeyboardFocus(VisTextField.this);
if (readOnly == false) keyboard.show(true);
hasSelection = true;
return true;
}
@Override
public void touchDragged (InputEvent event, float x, float y, int pointer) {
super.touchDragged(event, x, y, pointer);
setCursorPosition(x, y);
}
@Override
public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
if (selectionStart == cursor) hasSelection = false;
super.touchUp(event, x, y, pointer, button);
}
protected void setCursorPosition (float x, float y) {
lastBlink = 0;
cursorOn = false;
cursor = Math.min(letterUnderCursor(x), text.length());
}
protected void goHome (boolean jump) {
cursor = 0;
}
protected void goEnd (boolean jump) {
cursor = text.length();
}
@Override
public boolean keyDown (InputEvent event, int keycode) {
if (disabled) return false;
lastBlink = 0;
cursorOn = false;
Stage stage = getStage();
if (stage == null || stage.getKeyboardFocus() != VisTextField.this) return false;
if (drawBorder == false) return false;
boolean repeat = false;
boolean ctrl = UIUtils.ctrl();
boolean jump = ctrl && !passwordMode;
if (ctrl) {
if (keycode == Keys.V && readOnly == false) {
paste(clipboard.getContents(), true);
repeat = true;
}
if (keycode == Keys.C || keycode == Keys.INSERT) {
copy();
return true;
}
if (keycode == Keys.X && readOnly == false) {
cut(true);
return true;
}
if (keycode == Keys.A) {
selectAll();
return true;
}
if (keycode == Keys.Z && readOnly == false) {
String oldText = text;
int oldCursorPos = getCursorPosition();
setText(undoText);
VisTextField.this.setCursorPosition(MathUtils.clamp(cursor, 0, undoText.length()));
undoText = oldText;
undoCursorPos = oldCursorPos;
updateDisplayText();
return true;
}
}
if (UIUtils.shift()) {
if (keycode == Keys.INSERT && readOnly == false) paste(clipboard.getContents(), true);
if (keycode == Keys.FORWARD_DEL && readOnly == false) cut(true);
selection:
{
int temp = cursor;
keys:
{
if (keycode == Keys.LEFT) {
moveCursor(false, jump);
repeat = true;
break keys;
}
if (keycode == Keys.RIGHT) {
moveCursor(true, jump);
repeat = true;
break keys;
}
if (keycode == Keys.HOME) {
goHome(jump);
break keys;
}
if (keycode == Keys.END) {
goEnd(jump);
break keys;
}
break selection;
}
if (!hasSelection) {
selectionStart = temp;
hasSelection = true;
}
}
} else {
// Cursor movement or other keys (kills selection).
if (keycode == Keys.LEFT) {
moveCursor(false, jump);
clearSelection();
repeat = true;
}
if (keycode == Keys.RIGHT) {
moveCursor(true, jump);
clearSelection();
repeat = true;
}
if (keycode == Keys.HOME) {
goHome(jump);
clearSelection();
}
if (keycode == Keys.END) {
goEnd(jump);
clearSelection();
}
}
cursor = MathUtils.clamp(cursor, 0, text.length());
if (repeat) {
scheduleKeyRepeatTask(keycode);
}
return true;
}
protected void scheduleKeyRepeatTask (int keycode) {
if (!keyRepeatTask.isScheduled() || keyRepeatTask.keycode != keycode) {
keyRepeatTask.keycode = keycode;
keyRepeatTask.cancel();
if (Gdx.input.isKeyPressed(keyRepeatTask.keycode)) { //issue #179
Timer.schedule(keyRepeatTask, keyRepeatInitialTime, keyRepeatTime);
}
}
}
@Override
public boolean keyUp (InputEvent event, int keycode) {
if (disabled) return false;
keyRepeatTask.cancel();
return true;
}
@Override
public boolean keyTyped (InputEvent event, char character) {
if (disabled || readOnly) return false;
// Disallow "typing" most ASCII control characters, which would show up as a space when onlyFontChars is true.
switch (character) {
case BACKSPACE:
case TAB:
case ENTER_ANDROID:
case ENTER_DESKTOP:
break;
default:
if (character < 32) return false;
}
Stage stage = getStage();
if (stage == null || stage.getKeyboardFocus() != VisTextField.this) return false;
if (UIUtils.isMac && Gdx.input.isKeyPressed(Keys.SYM)) return true;
if (focusTraversal && (character == TAB || (character == ENTER_ANDROID && enterKeyFocusTraversal))) {
next(UIUtils.shift());
} else {
boolean delete = character == DELETE;
boolean backspace = character == BACKSPACE;
boolean enter = character == ENTER_DESKTOP || character == ENTER_ANDROID;
boolean add = enter ? writeEnters : (!onlyFontChars || style.font.getData().hasGlyph(character));
boolean remove = backspace || delete;
if (add || remove) {
String oldText = text;
int oldCursor = cursor;
if (hasSelection)
cursor = delete(false);
else {
if (backspace && cursor > 0) {
text = text.substring(0, cursor - 1) + text.substring(cursor--);
renderOffset = 0;
}
if (delete && cursor < text.length()) {
text = text.substring(0, cursor) + text.substring(cursor + 1);
}
}
if (add && !remove) {
// Character may be added to the text.
if (!enter && filter != null && !filter.acceptChar(VisTextField.this, character)) return true;
if (!withinMaxLength(text.length())) return true;
String insertion = enter ? "\n" : String.valueOf(character);
text = insert(cursor++, insertion, text);
}
if (changeText(oldText, text)) {
long time = System.currentTimeMillis();
if (time - 750 > lastChangeTime) {
undoText = oldText;
undoCursorPos = getCursorPosition() - 1;
}
lastChangeTime = time;
} else
cursor = oldCursor;
updateDisplayText();
}
}
if (listener != null) listener.keyTyped(VisTextField.this, character);
return true;
}
}
}