Home | History | Annotate | Download | only in ui
      1 /*******************************************************************************
      2  * Copyright 2011 See AUTHORS file.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *   http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  ******************************************************************************/
     16 
     17 package com.badlogic.gdx.scenes.scene2d.ui;
     18 
     19 import com.badlogic.gdx.Gdx;
     20 import com.badlogic.gdx.Input;
     21 import com.badlogic.gdx.Input.Keys;
     22 import com.badlogic.gdx.graphics.Color;
     23 import com.badlogic.gdx.graphics.g2d.Batch;
     24 import com.badlogic.gdx.graphics.g2d.BitmapFont;
     25 import com.badlogic.gdx.graphics.g2d.BitmapFont.BitmapFontData;
     26 import com.badlogic.gdx.graphics.g2d.GlyphLayout;
     27 import com.badlogic.gdx.graphics.g2d.GlyphLayout.GlyphRun;
     28 import com.badlogic.gdx.math.MathUtils;
     29 import com.badlogic.gdx.math.Vector2;
     30 import com.badlogic.gdx.scenes.scene2d.Actor;
     31 import com.badlogic.gdx.scenes.scene2d.Group;
     32 import com.badlogic.gdx.scenes.scene2d.InputEvent;
     33 import com.badlogic.gdx.scenes.scene2d.InputListener;
     34 import com.badlogic.gdx.scenes.scene2d.Stage;
     35 import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent;
     36 import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
     37 import com.badlogic.gdx.scenes.scene2d.utils.Disableable;
     38 import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
     39 import com.badlogic.gdx.scenes.scene2d.utils.UIUtils;
     40 import com.badlogic.gdx.utils.Align;
     41 import com.badlogic.gdx.utils.Array;
     42 import com.badlogic.gdx.utils.Clipboard;
     43 import com.badlogic.gdx.utils.FloatArray;
     44 import com.badlogic.gdx.utils.Pools;
     45 import com.badlogic.gdx.utils.TimeUtils;
     46 import com.badlogic.gdx.utils.Timer;
     47 import com.badlogic.gdx.utils.Timer.Task;
     48 
     49 /** A single-line text input field.
     50  * <p>
     51  * The preferred height of a text field is the height of the {@link TextFieldStyle#font} and {@link TextFieldStyle#background}.
     52  * The preferred width of a text field is 150, a relatively arbitrary size.
     53  * <p>
     54  * The text field will copy the currently selected text when ctrl+c is pressed, and paste any text in the clipboard when ctrl+v is
     55  * pressed. Clipboard functionality is provided via the {@link Clipboard} interface. Currently there are two standard
     56  * implementations, one for the desktop and one for Android. The Android clipboard is a stub, as copy & pasting on Android is not
     57  * supported yet.
     58  * <p>
     59  * The text field allows you to specify an {@link OnscreenKeyboard} for displaying a softkeyboard and piping all key events
     60  * generated by the keyboard to the text field. There are two standard implementations, one for the desktop and one for Android.
     61  * The desktop keyboard is a stub, as a softkeyboard is not needed on the desktop. The Android {@link OnscreenKeyboard}
     62  * implementation will bring up the default IME.
     63  * @author mzechner
     64  * @author Nathan Sweet */
     65 public class TextField extends Widget implements Disableable {
     66 	static private final char BACKSPACE = 8;
     67 	static protected final char ENTER_DESKTOP = '\r';
     68 	static protected final char ENTER_ANDROID = '\n';
     69 	static private final char TAB = '\t';
     70 	static private final char DELETE = 127;
     71 	static private final char BULLET = 149;
     72 
     73 	static private final Vector2 tmp1 = new Vector2();
     74 	static private final Vector2 tmp2 = new Vector2();
     75 	static private final Vector2 tmp3 = new Vector2();
     76 
     77 	static public float keyRepeatInitialTime = 0.4f;
     78 	static public float keyRepeatTime = 0.1f;
     79 
     80 	protected String text;
     81 	protected int cursor, selectionStart;
     82 	protected boolean hasSelection;
     83 	protected boolean writeEnters;
     84 	protected final GlyphLayout layout = new GlyphLayout();
     85 	protected final FloatArray glyphPositions = new FloatArray();
     86 
     87 	TextFieldStyle style;
     88 	private String messageText;
     89 	protected CharSequence displayText;
     90 	Clipboard clipboard;
     91 	InputListener inputListener;
     92 	TextFieldListener listener;
     93 	TextFieldFilter filter;
     94 	OnscreenKeyboard keyboard = new DefaultOnscreenKeyboard();
     95 	boolean focusTraversal = true, onlyFontChars = true, disabled;
     96 	private int textHAlign = Align.left;
     97 	private float selectionX, selectionWidth;
     98 
     99 	String undoText = "";
    100 	long lastChangeTime;
    101 
    102 	boolean passwordMode;
    103 	private StringBuilder passwordBuffer;
    104 	private char passwordCharacter = BULLET;
    105 
    106 	protected float fontOffset, textHeight, textOffset;
    107 	float renderOffset;
    108 	private int visibleTextStart, visibleTextEnd;
    109 	private int maxLength = 0;
    110 
    111 	private float blinkTime = 0.32f;
    112 	boolean cursorOn = true;
    113 	long lastBlink;
    114 
    115 	KeyRepeatTask keyRepeatTask = new KeyRepeatTask();
    116 	boolean programmaticChangeEvents;
    117 
    118 	public TextField (String text, Skin skin) {
    119 		this(text, skin.get(TextFieldStyle.class));
    120 	}
    121 
    122 	public TextField (String text, Skin skin, String styleName) {
    123 		this(text, skin.get(styleName, TextFieldStyle.class));
    124 	}
    125 
    126 	public TextField (String text, TextFieldStyle style) {
    127 		setStyle(style);
    128 		clipboard = Gdx.app.getClipboard();
    129 		initialize();
    130 		setText(text);
    131 		setSize(getPrefWidth(), getPrefHeight());
    132 	}
    133 
    134 	protected void initialize () {
    135 		addListener(inputListener = createInputListener());
    136 	}
    137 
    138 	protected InputListener createInputListener () {
    139 		return new TextFieldClickListener();
    140 	}
    141 
    142 	protected int letterUnderCursor (float x) {
    143 		x -= textOffset + fontOffset - style.font.getData().cursorX - glyphPositions.get(visibleTextStart);
    144 		int n = this.glyphPositions.size;
    145 		float[] glyphPositions = this.glyphPositions.items;
    146 		for (int i = 1; i < n; i++) {
    147 			if (glyphPositions[i] > x) {
    148 				if (glyphPositions[i] - x <= x - glyphPositions[i - 1]) return i;
    149 				return i - 1;
    150 			}
    151 		}
    152 		return n - 1;
    153 	}
    154 
    155 	protected boolean isWordCharacter (char c) {
    156 		return Character.isLetterOrDigit(c);
    157 	}
    158 
    159 	protected int[] wordUnderCursor (int at) {
    160 		String text = this.text;
    161 		int start = at, right = text.length(), left = 0, index = start;
    162 		for (; index < right; index++) {
    163 			if (!isWordCharacter(text.charAt(index))) {
    164 				right = index;
    165 				break;
    166 			}
    167 		}
    168 		for (index = start - 1; index > -1; index--) {
    169 			if (!isWordCharacter(text.charAt(index))) {
    170 				left = index + 1;
    171 				break;
    172 			}
    173 		}
    174 		return new int[] {left, right};
    175 	}
    176 
    177 	int[] wordUnderCursor (float x) {
    178 		return wordUnderCursor(letterUnderCursor(x));
    179 	}
    180 
    181 	boolean withinMaxLength (int size) {
    182 		return maxLength <= 0 || size < maxLength;
    183 	}
    184 
    185 	public void setMaxLength (int maxLength) {
    186 		this.maxLength = maxLength;
    187 	}
    188 
    189 	public int getMaxLength () {
    190 		return this.maxLength;
    191 	}
    192 
    193 	/** When false, text set by {@link #setText(String)} may contain characters not in the font, a space will be displayed instead.
    194 	 * When true (the default), characters not in the font are stripped by setText. Characters not in the font are always stripped
    195 	 * when typed or pasted. */
    196 	public void setOnlyFontChars (boolean onlyFontChars) {
    197 		this.onlyFontChars = onlyFontChars;
    198 	}
    199 
    200 	public void setStyle (TextFieldStyle style) {
    201 		if (style == null) throw new IllegalArgumentException("style cannot be null.");
    202 		this.style = style;
    203 		textHeight = style.font.getCapHeight() - style.font.getDescent() * 2;
    204 		invalidateHierarchy();
    205 	}
    206 
    207 	/** Returns the text field's style. Modifying the returned style may not have an effect until {@link #setStyle(TextFieldStyle)}
    208 	 * is called. */
    209 	public TextFieldStyle getStyle () {
    210 		return style;
    211 	}
    212 
    213 	protected void calculateOffsets () {
    214 		float visibleWidth = getWidth();
    215 		if (style.background != null) visibleWidth -= style.background.getLeftWidth() + style.background.getRightWidth();
    216 
    217 		int glyphCount = glyphPositions.size;
    218 		float[] glyphPositions = this.glyphPositions.items;
    219 
    220 		// Check if the cursor has gone out the left or right side of the visible area and adjust renderoffset.
    221 		float distance = glyphPositions[Math.max(0, cursor - 1)] + renderOffset;
    222 		if (distance <= 0)
    223 			renderOffset -= distance;
    224 		else {
    225 			int index = Math.min(glyphCount - 1, cursor + 1);
    226 			float minX = glyphPositions[index] - visibleWidth;
    227 			if (-renderOffset < minX) {
    228 				renderOffset = -minX;
    229 			}
    230 		}
    231 
    232 		// calculate first visible char based on render offset
    233 		visibleTextStart = 0;
    234 		float startX = 0;
    235 		for (int i = 0; i < glyphCount; i++) {
    236 			if (glyphPositions[i] >= -renderOffset) {
    237 				visibleTextStart = Math.max(0, i);
    238 				startX = glyphPositions[i];
    239 				break;
    240 			}
    241 		}
    242 
    243 		// calculate last visible char based on visible width and render offset
    244 		int length = displayText.length();
    245 		visibleTextEnd = Math.min(length, cursor + 1);
    246 		for (; visibleTextEnd <= length; visibleTextEnd++)
    247 			if (glyphPositions[visibleTextEnd] > startX + visibleWidth) break;
    248 		visibleTextEnd = Math.max(0, visibleTextEnd - 1);
    249 
    250 		if ((textHAlign & Align.left) == 0) {
    251 			textOffset = visibleWidth - (glyphPositions[visibleTextEnd] - startX);
    252 			if ((textHAlign & Align.center) != 0) textOffset = Math.round(textOffset * 0.5f);
    253 		} else
    254 			textOffset = startX + renderOffset;
    255 
    256 		// calculate selection x position and width
    257 		if (hasSelection) {
    258 			int minIndex = Math.min(cursor, selectionStart);
    259 			int maxIndex = Math.max(cursor, selectionStart);
    260 			float minX = Math.max(glyphPositions[minIndex], -renderOffset);
    261 			float maxX = Math.min(glyphPositions[maxIndex], visibleWidth - renderOffset);
    262 			selectionX = minX;
    263 			if (renderOffset == 0) selectionX += textOffset;
    264 			selectionWidth = maxX - minX - style.font.getData().cursorX;
    265 		}
    266 	}
    267 
    268 	@Override
    269 	public void draw (Batch batch, float parentAlpha) {
    270 		Stage stage = getStage();
    271 		boolean focused = stage != null && stage.getKeyboardFocus() == this;
    272 		if (!focused) keyRepeatTask.cancel();
    273 
    274 		final BitmapFont font = style.font;
    275 		final Color fontColor = (disabled && style.disabledFontColor != null) ? style.disabledFontColor
    276 			: ((focused && style.focusedFontColor != null) ? style.focusedFontColor : style.fontColor);
    277 		final Drawable selection = style.selection;
    278 		final Drawable cursorPatch = style.cursor;
    279 		final Drawable background = (disabled && style.disabledBackground != null) ? style.disabledBackground
    280 			: ((focused && style.focusedBackground != null) ? style.focusedBackground : style.background);
    281 
    282 		Color color = getColor();
    283 		float x = getX();
    284 		float y = getY();
    285 		float width = getWidth();
    286 		float height = getHeight();
    287 
    288 		batch.setColor(color.r, color.g, color.b, color.a * parentAlpha);
    289 		float bgLeftWidth = 0, bgRightWidth = 0;
    290 		if (background != null) {
    291 			background.draw(batch, x, y, width, height);
    292 			bgLeftWidth = background.getLeftWidth();
    293 			bgRightWidth = background.getRightWidth();
    294 		}
    295 
    296 		float textY = getTextY(font, background);
    297 		calculateOffsets();
    298 
    299 		if (focused && hasSelection && selection != null) {
    300 			drawSelection(selection, batch, font, x + bgLeftWidth, y + textY);
    301 		}
    302 
    303 		float yOffset = font.isFlipped() ? -textHeight : 0;
    304 		if (displayText.length() == 0) {
    305 			if (!focused && messageText != null) {
    306 				if (style.messageFontColor != null) {
    307 					font.setColor(style.messageFontColor.r, style.messageFontColor.g, style.messageFontColor.b,
    308 						style.messageFontColor.a * color.a * parentAlpha);
    309 				} else
    310 					font.setColor(0.7f, 0.7f, 0.7f, color.a * parentAlpha);
    311 				BitmapFont messageFont = style.messageFont != null ? style.messageFont : font;
    312 				messageFont.draw(batch, messageText, x + bgLeftWidth, y + textY + yOffset, 0, messageText.length(),
    313 					width - bgLeftWidth - bgRightWidth, textHAlign, false, "...");
    314 			}
    315 		} else {
    316 			font.setColor(fontColor.r, fontColor.g, fontColor.b, fontColor.a * color.a * parentAlpha);
    317 			drawText(batch, font, x + bgLeftWidth, y + textY + yOffset);
    318 		}
    319 		if (focused && !disabled) {
    320 			blink();
    321 			if (cursorOn && cursorPatch != null) {
    322 				drawCursor(cursorPatch, batch, font, x + bgLeftWidth, y + textY);
    323 			}
    324 		}
    325 	}
    326 
    327 	protected float getTextY (BitmapFont font, Drawable background) {
    328 		float height = getHeight();
    329 		float textY = textHeight / 2 + font.getDescent();
    330 		if (background != null) {
    331 			float bottom = background.getBottomHeight();
    332 			textY = textY + (height - background.getTopHeight() - bottom) / 2 + bottom;
    333 		} else {
    334 			textY = textY + height / 2;
    335 		}
    336 		if (font.usesIntegerPositions()) textY = (int)textY;
    337 		return textY;
    338 	}
    339 
    340 	/** Draws selection rectangle **/
    341 	protected void drawSelection (Drawable selection, Batch batch, BitmapFont font, float x, float y) {
    342 		selection.draw(batch, x + selectionX + renderOffset + fontOffset, y - textHeight - font.getDescent(), selectionWidth,
    343 			textHeight);
    344 	}
    345 
    346 	protected void drawText (Batch batch, BitmapFont font, float x, float y) {
    347 		font.draw(batch, displayText, x + textOffset, y, visibleTextStart, visibleTextEnd, 0, Align.left, false);
    348 	}
    349 
    350 	protected void drawCursor (Drawable cursorPatch, Batch batch, BitmapFont font, float x, float y) {
    351 		cursorPatch.draw(batch,
    352 			x + textOffset + glyphPositions.get(cursor) - glyphPositions.get(visibleTextStart) + fontOffset + font.getData().cursorX,
    353 			y - textHeight - font.getDescent(), cursorPatch.getMinWidth(), textHeight);
    354 	}
    355 
    356 	void updateDisplayText () {
    357 		BitmapFont font = style.font;
    358 		BitmapFontData data = font.getData();
    359 		String text = this.text;
    360 		int textLength = text.length();
    361 
    362 		StringBuilder buffer = new StringBuilder();
    363 		for (int i = 0; i < textLength; i++) {
    364 			char c = text.charAt(i);
    365 			buffer.append(data.hasGlyph(c) ? c : ' ');
    366 		}
    367 		String newDisplayText = buffer.toString();
    368 
    369 		if (passwordMode && data.hasGlyph(passwordCharacter)) {
    370 			if (passwordBuffer == null) passwordBuffer = new StringBuilder(newDisplayText.length());
    371 			if (passwordBuffer.length() > textLength)
    372 				passwordBuffer.setLength(textLength);
    373 			else {
    374 				for (int i = passwordBuffer.length(); i < textLength; i++)
    375 					passwordBuffer.append(passwordCharacter);
    376 			}
    377 			displayText = passwordBuffer;
    378 		} else
    379 			displayText = newDisplayText;
    380 
    381 		layout.setText(font, displayText);
    382 		glyphPositions.clear();
    383 		float x = 0;
    384 		if (layout.runs.size > 0) {
    385 			GlyphRun run = layout.runs.first();
    386 			FloatArray xAdvances = run.xAdvances;
    387 			fontOffset = xAdvances.first();
    388 			for (int i = 1, n = xAdvances.size; i < n; i++) {
    389 				glyphPositions.add(x);
    390 				x += xAdvances.get(i);
    391 			}
    392 		} else
    393 			fontOffset = 0;
    394 		glyphPositions.add(x);
    395 
    396 		if (selectionStart > newDisplayText.length()) selectionStart = textLength;
    397 	}
    398 
    399 	private void blink () {
    400 		if (!Gdx.graphics.isContinuousRendering()) {
    401 			cursorOn = true;
    402 			return;
    403 		}
    404 		long time = TimeUtils.nanoTime();
    405 		if ((time - lastBlink) / 1000000000.0f > blinkTime) {
    406 			cursorOn = !cursorOn;
    407 			lastBlink = time;
    408 		}
    409 	}
    410 
    411 	/** Copies the contents of this TextField to the {@link Clipboard} implementation set on this TextField. */
    412 	public void copy () {
    413 		if (hasSelection && !passwordMode) {
    414 			clipboard.setContents(text.substring(Math.min(cursor, selectionStart), Math.max(cursor, selectionStart)));
    415 		}
    416 	}
    417 
    418 	/** Copies the selected contents of this TextField to the {@link Clipboard} implementation set on this TextField, then removes
    419 	 * it. */
    420 	public void cut () {
    421 		cut(programmaticChangeEvents);
    422 	}
    423 
    424 	void cut (boolean fireChangeEvent) {
    425 		if (hasSelection && !passwordMode) {
    426 			copy();
    427 			cursor = delete(fireChangeEvent);
    428 			updateDisplayText();
    429 		}
    430 	}
    431 
    432 	void paste (String content, boolean fireChangeEvent) {
    433 		if (content == null) return;
    434 		StringBuilder buffer = new StringBuilder();
    435 		int textLength = text.length();
    436 		if (hasSelection) textLength -= Math.abs(cursor - selectionStart);
    437 		BitmapFontData data = style.font.getData();
    438 		for (int i = 0, n = content.length(); i < n; i++) {
    439 			if (!withinMaxLength(textLength + buffer.length())) break;
    440 			char c = content.charAt(i);
    441 			if (!(writeEnters && (c == ENTER_ANDROID || c == ENTER_DESKTOP))) {
    442 				if (c == '\r' || c == '\n') continue;
    443 				if (onlyFontChars && !data.hasGlyph(c)) continue;
    444 				if (filter != null && !filter.acceptChar(this, c)) continue;
    445 			}
    446 			buffer.append(c);
    447 		}
    448 		content = buffer.toString();
    449 
    450 		if (hasSelection) cursor = delete(fireChangeEvent);
    451 		if (fireChangeEvent)
    452 			changeText(text, insert(cursor, content, text));
    453 		else
    454 			text = insert(cursor, content, text);
    455 		updateDisplayText();
    456 		cursor += content.length();
    457 	}
    458 
    459 	String insert (int position, CharSequence text, String to) {
    460 		if (to.length() == 0) return text.toString();
    461 		return to.substring(0, position) + text + to.substring(position, to.length());
    462 	}
    463 
    464 	int delete (boolean fireChangeEvent) {
    465 		int from = selectionStart;
    466 		int to = cursor;
    467 		int minIndex = Math.min(from, to);
    468 		int maxIndex = Math.max(from, to);
    469 		String newText = (minIndex > 0 ? text.substring(0, minIndex) : "")
    470 			+ (maxIndex < text.length() ? text.substring(maxIndex, text.length()) : "");
    471 		if (fireChangeEvent)
    472 			changeText(text, newText);
    473 		else
    474 			text = newText;
    475 		clearSelection();
    476 		return minIndex;
    477 	}
    478 
    479 	/** Focuses the next TextField. If none is found, the keyboard is hidden. Does nothing if the text field is not in a stage.
    480 	 * @param up If true, the TextField with the same or next smallest y coordinate is found, else the next highest. */
    481 	public void next (boolean up) {
    482 		Stage stage = getStage();
    483 		if (stage == null) return;
    484 		getParent().localToStageCoordinates(tmp1.set(getX(), getY()));
    485 		TextField textField = findNextTextField(stage.getActors(), null, tmp2, tmp1, up);
    486 		if (textField == null) { // Try to wrap around.
    487 			if (up)
    488 				tmp1.set(Float.MIN_VALUE, Float.MIN_VALUE);
    489 			else
    490 				tmp1.set(Float.MAX_VALUE, Float.MAX_VALUE);
    491 			textField = findNextTextField(getStage().getActors(), null, tmp2, tmp1, up);
    492 		}
    493 		if (textField != null)
    494 			stage.setKeyboardFocus(textField);
    495 		else
    496 			Gdx.input.setOnscreenKeyboardVisible(false);
    497 	}
    498 
    499 	private TextField findNextTextField (Array<Actor> actors, TextField best, Vector2 bestCoords, Vector2 currentCoords,
    500 		boolean up) {
    501 		for (int i = 0, n = actors.size; i < n; i++) {
    502 			Actor actor = actors.get(i);
    503 			if (actor == this) continue;
    504 			if (actor instanceof TextField) {
    505 				TextField textField = (TextField)actor;
    506 				if (textField.isDisabled() || !textField.focusTraversal) continue;
    507 				Vector2 actorCoords = actor.getParent().localToStageCoordinates(tmp3.set(actor.getX(), actor.getY()));
    508 				if ((actorCoords.y < currentCoords.y || (actorCoords.y == currentCoords.y && actorCoords.x > currentCoords.x)) ^ up) {
    509 					if (best == null
    510 						|| (actorCoords.y > bestCoords.y || (actorCoords.y == bestCoords.y && actorCoords.x < bestCoords.x)) ^ up) {
    511 						best = (TextField)actor;
    512 						bestCoords.set(actorCoords);
    513 					}
    514 				}
    515 			} else if (actor instanceof Group)
    516 				best = findNextTextField(((Group)actor).getChildren(), best, bestCoords, currentCoords, up);
    517 		}
    518 		return best;
    519 	}
    520 
    521 	public InputListener getDefaultInputListener () {
    522 		return inputListener;
    523 	}
    524 
    525 	/** @param listener May be null. */
    526 	public void setTextFieldListener (TextFieldListener listener) {
    527 		this.listener = listener;
    528 	}
    529 
    530 	/** @param filter May be null. */
    531 	public void setTextFieldFilter (TextFieldFilter filter) {
    532 		this.filter = filter;
    533 	}
    534 
    535 	public TextFieldFilter getTextFieldFilter () {
    536 		return filter;
    537 	}
    538 
    539 	/** If true (the default), tab/shift+tab will move to the next text field. */
    540 	public void setFocusTraversal (boolean focusTraversal) {
    541 		this.focusTraversal = focusTraversal;
    542 	}
    543 
    544 	/** @return May be null. */
    545 	public String getMessageText () {
    546 		return messageText;
    547 	}
    548 
    549 	/** Sets the text that will be drawn in the text field if no text has been entered.
    550 	 * @param messageText may be null. */
    551 	public void setMessageText (String messageText) {
    552 		this.messageText = messageText;
    553 	}
    554 
    555 	/** @param str If null, "" is used. */
    556 	public void appendText (String str) {
    557 		if (str == null) str = "";
    558 
    559 		clearSelection();
    560 		cursor = text.length();
    561 		paste(str, programmaticChangeEvents);
    562 	}
    563 
    564 	/** @param str If null, "" is used. */
    565 	public void setText (String str) {
    566 		if (str == null) str = "";
    567 		if (str.equals(text)) return;
    568 
    569 		clearSelection();
    570 		String oldText = text;
    571 		text = "";
    572 		paste(str, false);
    573 		if (programmaticChangeEvents) changeText(oldText, text);
    574 		cursor = 0;
    575 	}
    576 
    577 	/** @return Never null, might be an empty string. */
    578 	public String getText () {
    579 		return text;
    580 	}
    581 
    582 	/** @param oldText May be null.
    583 	 * @return True if the text was changed. */
    584 	boolean changeText (String oldText, String newText) {
    585 		if (newText.equals(oldText)) return false;
    586 		text = newText;
    587 		ChangeEvent changeEvent = Pools.obtain(ChangeEvent.class);
    588 		boolean cancelled = fire(changeEvent);
    589 		text = cancelled ? oldText : newText;
    590 		Pools.free(changeEvent);
    591 		return !cancelled;
    592 	}
    593 
    594 	/** If false, methods that change the text will not fire {@link ChangeEvent}, the event will be fired only when user changes
    595 	 * the text. */
    596 	public void setProgrammaticChangeEvents (boolean programmaticChangeEvents) {
    597 		this.programmaticChangeEvents = programmaticChangeEvents;
    598 	}
    599 
    600 	public int getSelectionStart () {
    601 		return selectionStart;
    602 	}
    603 
    604 	public String getSelection () {
    605 		return hasSelection ? text.substring(Math.min(selectionStart, cursor), Math.max(selectionStart, cursor)) : "";
    606 	}
    607 
    608 	/** Sets the selected text. */
    609 	public void setSelection (int selectionStart, int selectionEnd) {
    610 		if (selectionStart < 0) throw new IllegalArgumentException("selectionStart must be >= 0");
    611 		if (selectionEnd < 0) throw new IllegalArgumentException("selectionEnd must be >= 0");
    612 		selectionStart = Math.min(text.length(), selectionStart);
    613 		selectionEnd = Math.min(text.length(), selectionEnd);
    614 		if (selectionEnd == selectionStart) {
    615 			clearSelection();
    616 			return;
    617 		}
    618 		if (selectionEnd < selectionStart) {
    619 			int temp = selectionEnd;
    620 			selectionEnd = selectionStart;
    621 			selectionStart = temp;
    622 		}
    623 
    624 		hasSelection = true;
    625 		this.selectionStart = selectionStart;
    626 		cursor = selectionEnd;
    627 	}
    628 
    629 	public void selectAll () {
    630 		setSelection(0, text.length());
    631 	}
    632 
    633 	public void clearSelection () {
    634 		hasSelection = false;
    635 	}
    636 
    637 	/** Sets the cursor position and clears any selection. */
    638 	public void setCursorPosition (int cursorPosition) {
    639 		if (cursorPosition < 0) throw new IllegalArgumentException("cursorPosition must be >= 0");
    640 		clearSelection();
    641 		cursor = Math.min(cursorPosition, text.length());
    642 	}
    643 
    644 	public int getCursorPosition () {
    645 		return cursor;
    646 	}
    647 
    648 	/** Default is an instance of {@link DefaultOnscreenKeyboard}. */
    649 	public OnscreenKeyboard getOnscreenKeyboard () {
    650 		return keyboard;
    651 	}
    652 
    653 	public void setOnscreenKeyboard (OnscreenKeyboard keyboard) {
    654 		this.keyboard = keyboard;
    655 	}
    656 
    657 	public void setClipboard (Clipboard clipboard) {
    658 		this.clipboard = clipboard;
    659 	}
    660 
    661 	public float getPrefWidth () {
    662 		return 150;
    663 	}
    664 
    665 	public float getPrefHeight () {
    666 		float prefHeight = textHeight;
    667 		if (style.background != null) {
    668 			prefHeight = Math.max(prefHeight + style.background.getBottomHeight() + style.background.getTopHeight(),
    669 				style.background.getMinHeight());
    670 		}
    671 		return prefHeight;
    672 	}
    673 
    674 	/** Sets text horizontal alignment (left, center or right).
    675 	 * @see Align */
    676 	public void setAlignment (int alignment) {
    677 		this.textHAlign = alignment;
    678 	}
    679 
    680 	/** If true, the text in this text field will be shown as bullet characters.
    681 	 * @see #setPasswordCharacter(char) */
    682 	public void setPasswordMode (boolean passwordMode) {
    683 		this.passwordMode = passwordMode;
    684 		updateDisplayText();
    685 	}
    686 
    687 	public boolean isPasswordMode () {
    688 		return passwordMode;
    689 	}
    690 
    691 	/** Sets the password character for the text field. The character must be present in the {@link BitmapFont}. Default is 149
    692 	 * (bullet). */
    693 	public void setPasswordCharacter (char passwordCharacter) {
    694 		this.passwordCharacter = passwordCharacter;
    695 		if (passwordMode) updateDisplayText();
    696 	}
    697 
    698 	public void setBlinkTime (float blinkTime) {
    699 		this.blinkTime = blinkTime;
    700 	}
    701 
    702 	public void setDisabled (boolean disabled) {
    703 		this.disabled = disabled;
    704 	}
    705 
    706 	public boolean isDisabled () {
    707 		return disabled;
    708 	}
    709 
    710 	protected void moveCursor (boolean forward, boolean jump) {
    711 		int limit = forward ? text.length() : 0;
    712 		int charOffset = forward ? 0 : -1;
    713 		while ((forward ? ++cursor < limit : --cursor > limit) && jump) {
    714 			if (!continueCursor(cursor, charOffset)) break;
    715 		}
    716 	}
    717 
    718 	protected boolean continueCursor (int index, int offset) {
    719 		char c = text.charAt(index + offset);
    720 		return isWordCharacter(c);
    721 	}
    722 
    723 	class KeyRepeatTask extends Task {
    724 		int keycode;
    725 
    726 		public void run () {
    727 			inputListener.keyDown(null, keycode);
    728 		}
    729 	}
    730 
    731 	/** Interface for listening to typed characters.
    732 	 * @author mzechner */
    733 	static public interface TextFieldListener {
    734 		public void keyTyped (TextField textField, char c);
    735 	}
    736 
    737 	/** Interface for filtering characters entered into the text field.
    738 	 * @author mzechner */
    739 	static public interface TextFieldFilter {
    740 		public boolean acceptChar (TextField textField, char c);
    741 
    742 		static public class DigitsOnlyFilter implements TextFieldFilter {
    743 			@Override
    744 			public boolean acceptChar (TextField textField, char c) {
    745 				return Character.isDigit(c);
    746 			}
    747 
    748 		}
    749 	}
    750 
    751 	/** An interface for onscreen keyboards. Can invoke the default keyboard or render your own keyboard!
    752 	 * @author mzechner */
    753 	static public interface OnscreenKeyboard {
    754 		public void show (boolean visible);
    755 	}
    756 
    757 	/** The default {@link OnscreenKeyboard} used by all {@link TextField} instances. Just uses
    758 	 * {@link Input#setOnscreenKeyboardVisible(boolean)} as appropriate. Might overlap your actual rendering, so use with care!
    759 	 * @author mzechner */
    760 	static public class DefaultOnscreenKeyboard implements OnscreenKeyboard {
    761 		@Override
    762 		public void show (boolean visible) {
    763 			Gdx.input.setOnscreenKeyboardVisible(visible);
    764 		}
    765 	}
    766 
    767 	/** Basic input listener for the text field */
    768 	public class TextFieldClickListener extends ClickListener {
    769 		public void clicked (InputEvent event, float x, float y) {
    770 			int count = getTapCount() % 4;
    771 			if (count == 0) clearSelection();
    772 			if (count == 2) {
    773 				int[] array = wordUnderCursor(x);
    774 				setSelection(array[0], array[1]);
    775 			}
    776 			if (count == 3) selectAll();
    777 		}
    778 
    779 		public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
    780 			if (!super.touchDown(event, x, y, pointer, button)) return false;
    781 			if (pointer == 0 && button != 0) return false;
    782 			if (disabled) return true;
    783 			setCursorPosition(x, y);
    784 			selectionStart = cursor;
    785 			Stage stage = getStage();
    786 			if (stage != null) stage.setKeyboardFocus(TextField.this);
    787 			keyboard.show(true);
    788 			hasSelection = true;
    789 			return true;
    790 		}
    791 
    792 		public void touchDragged (InputEvent event, float x, float y, int pointer) {
    793 			super.touchDragged(event, x, y, pointer);
    794 			setCursorPosition(x, y);
    795 		}
    796 
    797 		public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
    798 			if (selectionStart == cursor) hasSelection = false;
    799 			super.touchUp(event, x, y, pointer, button);
    800 		}
    801 
    802 		protected void setCursorPosition (float x, float y) {
    803 			lastBlink = 0;
    804 			cursorOn = false;
    805 			cursor = letterUnderCursor(x);
    806 		}
    807 
    808 		protected void goHome (boolean jump) {
    809 			cursor = 0;
    810 		}
    811 
    812 		protected void goEnd (boolean jump) {
    813 			cursor = text.length();
    814 		}
    815 
    816 		public boolean keyDown (InputEvent event, int keycode) {
    817 			if (disabled) return false;
    818 
    819 			lastBlink = 0;
    820 			cursorOn = false;
    821 
    822 			Stage stage = getStage();
    823 			if (stage == null || stage.getKeyboardFocus() != TextField.this) return false;
    824 
    825 			boolean repeat = false;
    826 			boolean ctrl = UIUtils.ctrl();
    827 			boolean jump = ctrl && !passwordMode;
    828 
    829 			if (ctrl) {
    830 				if (keycode == Keys.V) {
    831 					paste(clipboard.getContents(), true);
    832 					repeat = true;
    833 				}
    834 				if (keycode == Keys.C || keycode == Keys.INSERT) {
    835 					copy();
    836 					return true;
    837 				}
    838 				if (keycode == Keys.X) {
    839 					cut(true);
    840 					return true;
    841 				}
    842 				if (keycode == Keys.A) {
    843 					selectAll();
    844 					return true;
    845 				}
    846 				if (keycode == Keys.Z) {
    847 					String oldText = text;
    848 					setText(undoText);
    849 					undoText = oldText;
    850 					updateDisplayText();
    851 					return true;
    852 				}
    853 			}
    854 
    855 			if (UIUtils.shift()) {
    856 				if (keycode == Keys.INSERT) paste(clipboard.getContents(), true);
    857 				if (keycode == Keys.FORWARD_DEL) cut(true);
    858 				selection:
    859 				{
    860 					int temp = cursor;
    861 					keys:
    862 					{
    863 						if (keycode == Keys.LEFT) {
    864 							moveCursor(false, jump);
    865 							repeat = true;
    866 							break keys;
    867 						}
    868 						if (keycode == Keys.RIGHT) {
    869 							moveCursor(true, jump);
    870 							repeat = true;
    871 							break keys;
    872 						}
    873 						if (keycode == Keys.HOME) {
    874 							goHome(jump);
    875 							break keys;
    876 						}
    877 						if (keycode == Keys.END) {
    878 							goEnd(jump);
    879 							break keys;
    880 						}
    881 						break selection;
    882 					}
    883 					if (!hasSelection) {
    884 						selectionStart = temp;
    885 						hasSelection = true;
    886 					}
    887 				}
    888 			} else {
    889 				// Cursor movement or other keys (kills selection).
    890 				if (keycode == Keys.LEFT) {
    891 					moveCursor(false, jump);
    892 					clearSelection();
    893 					repeat = true;
    894 				}
    895 				if (keycode == Keys.RIGHT) {
    896 					moveCursor(true, jump);
    897 					clearSelection();
    898 					repeat = true;
    899 				}
    900 				if (keycode == Keys.HOME) {
    901 					goHome(jump);
    902 					clearSelection();
    903 				}
    904 				if (keycode == Keys.END) {
    905 					goEnd(jump);
    906 					clearSelection();
    907 				}
    908 			}
    909 			cursor = MathUtils.clamp(cursor, 0, text.length());
    910 
    911 			if (repeat) {
    912 				scheduleKeyRepeatTask(keycode);
    913 			}
    914 			return true;
    915 		}
    916 
    917 		protected void scheduleKeyRepeatTask (int keycode) {
    918 			if (!keyRepeatTask.isScheduled() || keyRepeatTask.keycode != keycode) {
    919 				keyRepeatTask.keycode = keycode;
    920 				keyRepeatTask.cancel();
    921 				Timer.schedule(keyRepeatTask, keyRepeatInitialTime, keyRepeatTime);
    922 			}
    923 		}
    924 
    925 		public boolean keyUp (InputEvent event, int keycode) {
    926 			if (disabled) return false;
    927 			keyRepeatTask.cancel();
    928 			return true;
    929 		}
    930 
    931 		public boolean keyTyped (InputEvent event, char character) {
    932 			if (disabled) return false;
    933 
    934 			// Disallow "typing" most ASCII control characters, which would show up as a space when onlyFontChars is true.
    935 			switch (character) {
    936 			case BACKSPACE:
    937 			case TAB:
    938 			case ENTER_ANDROID:
    939 			case ENTER_DESKTOP:
    940 				break;
    941 			default:
    942 				if (character < 32) return false;
    943 			}
    944 
    945 			Stage stage = getStage();
    946 			if (stage == null || stage.getKeyboardFocus() != TextField.this) return false;
    947 
    948 			if (UIUtils.isMac && Gdx.input.isKeyPressed(Keys.SYM)) return true;
    949 
    950 			if ((character == TAB || character == ENTER_ANDROID) && focusTraversal) {
    951 				next(UIUtils.shift());
    952 			} else {
    953 				boolean delete = character == DELETE;
    954 				boolean backspace = character == BACKSPACE;
    955 				boolean enter = character == ENTER_DESKTOP || character == ENTER_ANDROID;
    956 				boolean add = enter ? writeEnters : (!onlyFontChars || style.font.getData().hasGlyph(character));
    957 				boolean remove = backspace || delete;
    958 				if (add || remove) {
    959 					String oldText = text;
    960 					int oldCursor = cursor;
    961 					if (hasSelection)
    962 						cursor = delete(false);
    963 					else {
    964 						if (backspace && cursor > 0) {
    965 							text = text.substring(0, cursor - 1) + text.substring(cursor--);
    966 							renderOffset = 0;
    967 						}
    968 						if (delete && cursor < text.length()) {
    969 							text = text.substring(0, cursor) + text.substring(cursor + 1);
    970 						}
    971 					}
    972 					if (add && !remove) {
    973 						// Character may be added to the text.
    974 						if (!enter && filter != null && !filter.acceptChar(TextField.this, character)) return true;
    975 						if (!withinMaxLength(text.length())) return true;
    976 						String insertion = enter ? "\n" : String.valueOf(character);
    977 						text = insert(cursor++, insertion, text);
    978 					}
    979 					String tempUndoText = undoText;
    980 					if (changeText(oldText, text)) {
    981 						long time = System.currentTimeMillis();
    982 						if (time - 750 > lastChangeTime) undoText = oldText;
    983 						lastChangeTime = time;
    984 					} else
    985 						cursor = oldCursor;
    986 					updateDisplayText();
    987 				}
    988 			}
    989 			if (listener != null) listener.keyTyped(TextField.this, character);
    990 			return true;
    991 		}
    992 	}
    993 
    994 	/** The style for a text field, see {@link TextField}.
    995 	 * @author mzechner
    996 	 * @author Nathan Sweet */
    997 	static public class TextFieldStyle {
    998 		public BitmapFont font;
    999 		public Color fontColor;
   1000 		/** Optional. */
   1001 		public Color focusedFontColor, disabledFontColor;
   1002 		/** Optional. */
   1003 		public Drawable background, focusedBackground, disabledBackground, cursor, selection;
   1004 		/** Optional. */
   1005 		public BitmapFont messageFont;
   1006 		/** Optional. */
   1007 		public Color messageFontColor;
   1008 
   1009 		public TextFieldStyle () {
   1010 		}
   1011 
   1012 		public TextFieldStyle (BitmapFont font, Color fontColor, Drawable cursor, Drawable selection, Drawable background) {
   1013 			this.background = background;
   1014 			this.cursor = cursor;
   1015 			this.font = font;
   1016 			this.fontColor = fontColor;
   1017 			this.selection = selection;
   1018 		}
   1019 
   1020 		public TextFieldStyle (TextFieldStyle style) {
   1021 			this.messageFont = style.messageFont;
   1022 			if (style.messageFontColor != null) this.messageFontColor = new Color(style.messageFontColor);
   1023 			this.background = style.background;
   1024 			this.focusedBackground = style.focusedBackground;
   1025 			this.disabledBackground = style.disabledBackground;
   1026 			this.cursor = style.cursor;
   1027 			this.font = style.font;
   1028 			if (style.fontColor != null) this.fontColor = new Color(style.fontColor);
   1029 			if (style.focusedFontColor != null) this.focusedFontColor = new Color(style.focusedFontColor);
   1030 			if (style.disabledFontColor != null) this.disabledFontColor = new Color(style.disabledFontColor);
   1031 			this.selection = style.selection;
   1032 		}
   1033 	}
   1034 }
   1035