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