• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
TextField(String text, Skin skin)118 	public TextField (String text, Skin skin) {
119 		this(text, skin.get(TextFieldStyle.class));
120 	}
121 
TextField(String text, Skin skin, String styleName)122 	public TextField (String text, Skin skin, String styleName) {
123 		this(text, skin.get(styleName, TextFieldStyle.class));
124 	}
125 
TextField(String text, TextFieldStyle style)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 
initialize()134 	protected void initialize () {
135 		addListener(inputListener = createInputListener());
136 	}
137 
createInputListener()138 	protected InputListener createInputListener () {
139 		return new TextFieldClickListener();
140 	}
141 
letterUnderCursor(float x)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 
isWordCharacter(char c)155 	protected boolean isWordCharacter (char c) {
156 		return Character.isLetterOrDigit(c);
157 	}
158 
wordUnderCursor(int at)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 
wordUnderCursor(float x)177 	int[] wordUnderCursor (float x) {
178 		return wordUnderCursor(letterUnderCursor(x));
179 	}
180 
withinMaxLength(int size)181 	boolean withinMaxLength (int size) {
182 		return maxLength <= 0 || size < maxLength;
183 	}
184 
setMaxLength(int maxLength)185 	public void setMaxLength (int maxLength) {
186 		this.maxLength = maxLength;
187 	}
188 
getMaxLength()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. */
setOnlyFontChars(boolean onlyFontChars)196 	public void setOnlyFontChars (boolean onlyFontChars) {
197 		this.onlyFontChars = onlyFontChars;
198 	}
199 
setStyle(TextFieldStyle style)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. */
getStyle()209 	public TextFieldStyle getStyle () {
210 		return style;
211 	}
212 
calculateOffsets()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
draw(Batch batch, float parentAlpha)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 
getTextY(BitmapFont font, Drawable background)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 **/
drawSelection(Drawable selection, Batch batch, BitmapFont font, float x, float y)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 
drawText(Batch batch, BitmapFont font, float x, float y)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 
drawCursor(Drawable cursorPatch, Batch batch, BitmapFont font, float x, float y)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 
updateDisplayText()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 
blink()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. */
copy()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. */
cut()420 	public void cut () {
421 		cut(programmaticChangeEvents);
422 	}
423 
cut(boolean fireChangeEvent)424 	void cut (boolean fireChangeEvent) {
425 		if (hasSelection && !passwordMode) {
426 			copy();
427 			cursor = delete(fireChangeEvent);
428 			updateDisplayText();
429 		}
430 	}
431 
paste(String content, boolean fireChangeEvent)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 
insert(int position, CharSequence text, String to)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 
delete(boolean fireChangeEvent)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. */
next(boolean up)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 
findNextTextField(Array<Actor> actors, TextField best, Vector2 bestCoords, Vector2 currentCoords, boolean up)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 
getDefaultInputListener()521 	public InputListener getDefaultInputListener () {
522 		return inputListener;
523 	}
524 
525 	/** @param listener May be null. */
setTextFieldListener(TextFieldListener listener)526 	public void setTextFieldListener (TextFieldListener listener) {
527 		this.listener = listener;
528 	}
529 
530 	/** @param filter May be null. */
setTextFieldFilter(TextFieldFilter filter)531 	public void setTextFieldFilter (TextFieldFilter filter) {
532 		this.filter = filter;
533 	}
534 
getTextFieldFilter()535 	public TextFieldFilter getTextFieldFilter () {
536 		return filter;
537 	}
538 
539 	/** If true (the default), tab/shift+tab will move to the next text field. */
setFocusTraversal(boolean focusTraversal)540 	public void setFocusTraversal (boolean focusTraversal) {
541 		this.focusTraversal = focusTraversal;
542 	}
543 
544 	/** @return May be null. */
getMessageText()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. */
setMessageText(String messageText)551 	public void setMessageText (String messageText) {
552 		this.messageText = messageText;
553 	}
554 
555 	/** @param str If null, "" is used. */
appendText(String str)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. */
setText(String str)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. */
getText()578 	public String getText () {
579 		return text;
580 	}
581 
582 	/** @param oldText May be null.
583 	 * @return True if the text was changed. */
changeText(String oldText, String newText)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. */
setProgrammaticChangeEvents(boolean programmaticChangeEvents)596 	public void setProgrammaticChangeEvents (boolean programmaticChangeEvents) {
597 		this.programmaticChangeEvents = programmaticChangeEvents;
598 	}
599 
getSelectionStart()600 	public int getSelectionStart () {
601 		return selectionStart;
602 	}
603 
getSelection()604 	public String getSelection () {
605 		return hasSelection ? text.substring(Math.min(selectionStart, cursor), Math.max(selectionStart, cursor)) : "";
606 	}
607 
608 	/** Sets the selected text. */
setSelection(int selectionStart, int selectionEnd)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 
selectAll()629 	public void selectAll () {
630 		setSelection(0, text.length());
631 	}
632 
clearSelection()633 	public void clearSelection () {
634 		hasSelection = false;
635 	}
636 
637 	/** Sets the cursor position and clears any selection. */
setCursorPosition(int cursorPosition)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 
getCursorPosition()644 	public int getCursorPosition () {
645 		return cursor;
646 	}
647 
648 	/** Default is an instance of {@link DefaultOnscreenKeyboard}. */
getOnscreenKeyboard()649 	public OnscreenKeyboard getOnscreenKeyboard () {
650 		return keyboard;
651 	}
652 
setOnscreenKeyboard(OnscreenKeyboard keyboard)653 	public void setOnscreenKeyboard (OnscreenKeyboard keyboard) {
654 		this.keyboard = keyboard;
655 	}
656 
setClipboard(Clipboard clipboard)657 	public void setClipboard (Clipboard clipboard) {
658 		this.clipboard = clipboard;
659 	}
660 
getPrefWidth()661 	public float getPrefWidth () {
662 		return 150;
663 	}
664 
getPrefHeight()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 */
setAlignment(int alignment)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) */
setPasswordMode(boolean passwordMode)682 	public void setPasswordMode (boolean passwordMode) {
683 		this.passwordMode = passwordMode;
684 		updateDisplayText();
685 	}
686 
isPasswordMode()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). */
setPasswordCharacter(char passwordCharacter)693 	public void setPasswordCharacter (char passwordCharacter) {
694 		this.passwordCharacter = passwordCharacter;
695 		if (passwordMode) updateDisplayText();
696 	}
697 
setBlinkTime(float blinkTime)698 	public void setBlinkTime (float blinkTime) {
699 		this.blinkTime = blinkTime;
700 	}
701 
setDisabled(boolean disabled)702 	public void setDisabled (boolean disabled) {
703 		this.disabled = disabled;
704 	}
705 
isDisabled()706 	public boolean isDisabled () {
707 		return disabled;
708 	}
709 
moveCursor(boolean forward, boolean jump)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 
continueCursor(int index, int offset)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 
run()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 {
keyTyped(TextField textField, char c)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 {
acceptChar(TextField textField, char c)740 		public boolean acceptChar (TextField textField, char c);
741 
742 		static public class DigitsOnlyFilter implements TextFieldFilter {
743 			@Override
acceptChar(TextField textField, char c)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 {
show(boolean visible)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
show(boolean visible)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 {
clicked(InputEvent event, float x, float y)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 
touchDown(InputEvent event, float x, float y, int pointer, int button)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 
touchDragged(InputEvent event, float x, float y, int pointer)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 
touchUp(InputEvent event, float x, float y, int pointer, int button)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 
setCursorPosition(float x, float y)802 		protected void setCursorPosition (float x, float y) {
803 			lastBlink = 0;
804 			cursorOn = false;
805 			cursor = letterUnderCursor(x);
806 		}
807 
goHome(boolean jump)808 		protected void goHome (boolean jump) {
809 			cursor = 0;
810 		}
811 
goEnd(boolean jump)812 		protected void goEnd (boolean jump) {
813 			cursor = text.length();
814 		}
815 
keyDown(InputEvent event, int keycode)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 
scheduleKeyRepeatTask(int keycode)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 
keyUp(InputEvent event, int keycode)925 		public boolean keyUp (InputEvent event, int keycode) {
926 			if (disabled) return false;
927 			keyRepeatTask.cancel();
928 			return true;
929 		}
930 
keyTyped(InputEvent event, char character)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 
TextFieldStyle()1009 		public TextFieldStyle () {
1010 		}
1011 
TextFieldStyle(BitmapFont font, Color fontColor, Drawable cursor, Drawable selection, Drawable background)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 
TextFieldStyle(TextFieldStyle style)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