2 * Copyright (c) 2008-2010, Matthias Mann
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
9 * * Redistributions of source code must retain the above copyright notice,
10 * this list of conditions and the following disclaimer.
11 * * Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * * Neither the name of Matthias Mann nor the names of its contributors may
15 * be used to endorse or promote products derived from this software
16 * without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
22 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 package de.matthiasmann.twl;
32 import de.matthiasmann.twl.model.StringModel;
33 import de.matthiasmann.twl.utils.TextUtil;
34 import de.matthiasmann.twl.utils.CallbackSupport;
35 import de.matthiasmann.twl.renderer.Font;
36 import de.matthiasmann.twl.renderer.Image;
37 import org.lwjgl.input.Keyboard;
40 * A simple one line edit field
42 * @author Matthias Mann
44 public class EditField extends Widget {
46 public static final String STATE_ERROR = "error";
47 public static final String STATE_READONLY = "readonly";
48 public static final String STATE_HOVER = "hover";
50 public interface Callback {
52 * Gets called for any change in the edit field, or when ESCAPE or RETURN was pressed
54 * @param key One of KEY_NONE, KEY_ESCAPE, KEY_RETURN
55 * @see Keyboard#KEY_NONE
56 * @see Keyboard#KEY_ESCAPE
57 * @see Keyboard#KEY_RETURN
59 public void callback(int key);
62 private final StringBuilder editBuffer;
63 private final TextRenderer textRenderer;
64 private PasswordMasker passwordMasking;
65 private Runnable modelChangeListener;
66 private StringModel model;
67 private boolean readOnly;
69 private int cursorPos;
70 private int scrollPos;
71 private int selectionStart;
72 private int selectionEnd;
73 private int maxTextLength = Short.MAX_VALUE;
75 private int columns = 5;
76 private Image cursorImage;
77 private Image selectionImage;
78 private char passwordChar;
79 private Object errorMsg;
80 private Callback[] callbacks;
81 private PopupMenu popupMenu;
82 private boolean textLongerThenWidget;
84 private InfoWindow autoCompletionWindow;
85 private boolean autoCompletionWantKeys;
86 private int autoCompletionHeight = 100;
88 private InfoWindow errorInfoWindow;
89 private Label errorInfoLabel;
92 * Creates a new EditField with an optional parent animation state.
94 * Unlike other widgets which use the passed animation state directly,
95 * the EditField always creates it's animation state with the passed
98 * @param parentAnimationState
99 * @see AnimationState#AnimationState(de.matthiasmann.twl.AnimationState)
101 public EditField(AnimationState parentAnimationState) {
102 super(parentAnimationState, true);
104 this.editBuffer = new StringBuilder();
105 this.textRenderer = new TextRenderer(getAnimationState());
106 this.passwordChar = '*';
108 textRenderer.setTheme("renderer");
109 textRenderer.setClip(true);
112 setCanAcceptKeyboardFocus(true);
113 setDepthFocusTraversal(false);
115 addActionMapping("cut", "cutToClipboard");
116 addActionMapping("copy", "copyToClipboard");
117 addActionMapping("paste", "pasteFromClipboard");
118 addActionMapping("selectAll", "selectAll");
125 public void addCallback(Callback cb) {
126 callbacks = CallbackSupport.addCallbackToList(callbacks, cb, Callback.class);
129 public void removeCallback(Callback cb) {
130 callbacks = CallbackSupport.removeCallbackFromList(callbacks, cb);
133 protected void doCallback(int key) {
134 if(callbacks != null) {
135 for(Callback cb : callbacks) {
141 public boolean isPasswordMasking() {
142 return passwordMasking != null;
145 public void setPasswordMasking(boolean passwordMasking) {
146 if(passwordMasking != isPasswordMasking()) {
147 if(passwordMasking) {
148 this.passwordMasking = new PasswordMasker(editBuffer, passwordChar);
150 this.passwordMasking = null;
156 public char getPasswordChar() {
160 public void setPasswordChar(char passwordChar) {
161 this.passwordChar = passwordChar;
162 if(passwordMasking != null) {
163 passwordMasking = new PasswordMasker(editBuffer, passwordChar);
168 public int getColumns() {
173 * This is used to determine the desired width of the EditField based on
174 * it's font and the character 'X'
176 * @param columns number of characters
177 * @throws IllegalArgumentException if columns < 0
179 public void setColumns(int columns) {
181 throw new IllegalArgumentException("columns");
183 this.columns = columns;
186 public StringModel getModel() {
190 public void setModel(StringModel model) {
191 if(this.model != null) {
192 this.model.removeCallback(modelChangeListener);
195 if(this.model != null) {
196 if(modelChangeListener == null) {
197 modelChangeListener = new ModelChangeListener();
199 this.model.addCallback(modelChangeListener);
204 public void setText(String text) {
205 text = TextUtil.limitStringLength(text, maxTextLength);
206 editBuffer.replace(0, editBuffer.length(), text);
207 cursorPos = editBuffer.length();
211 scrollToCursor(true);
214 public String getText() {
215 return editBuffer.toString();
218 public String getSelectedText() {
219 return editBuffer.substring(selectionStart, selectionEnd);
222 public boolean hasSelection() {
223 return selectionStart != selectionEnd;
226 public int getCursorPos() {
230 public int getTextLength() {
231 return editBuffer.length();
234 public boolean isReadOnly() {
238 public void setReadOnly(boolean readOnly) {
239 this.readOnly = readOnly;
240 getAnimationState().setAnimationState(STATE_READONLY, readOnly);
243 public void insertText(String str) {
245 boolean update = false;
250 int insertLength = Math.min(str.length(), maxTextLength - editBuffer.length());
251 if(insertLength > 0) {
252 editBuffer.insert(cursorPos, str, 0, insertLength);
253 cursorPos += insertLength;
262 public void pasteFromClipboard() {
263 String cbText = Clipboard.getClipboard();
265 cbText = TextUtil.stripNewLines(cbText);
270 public void copyToClipboard() {
273 text = getSelectedText();
277 if(isPasswordMasking()) {
278 text = TextUtil.createString(passwordChar, text.length());
280 Clipboard.setClipboard(text);
283 public void cutToClipboard() {
286 text = getSelectedText();
297 if(isPasswordMasking()) {
298 text = TextUtil.createString(passwordChar, text.length());
300 Clipboard.setClipboard(text);
303 public int getMaxTextLength() {
304 return maxTextLength;
307 public void setMaxTextLength(int maxTextLength) {
308 this.maxTextLength = maxTextLength;
312 protected void applyTheme(ThemeInfo themeInfo) {
313 super.applyTheme(themeInfo);
314 applayThemeEditField(themeInfo);
317 protected void applayThemeEditField(ThemeInfo themeInfo) {
318 cursorImage = themeInfo.getImage("cursor");
319 selectionImage = themeInfo.getImage("selection");
320 autoCompletionHeight = themeInfo.getParameter("autocompletion-height", 100);
321 columns = themeInfo.getParameter("columns", 5);
322 setPasswordChar((char)themeInfo.getParameter("passwordChar", '*'));
323 setErrorMessage(errorMsg); // update color
327 protected void layout() {
328 layoutChildFullInnerArea(textRenderer);
330 if(autoCompletionWindow != null) {
331 layoutAutocompletionWindow();
333 if(errorInfoWindow != null) {
334 layoutErrorInfoWindow();
338 private void layoutAutocompletionWindow() {
339 autoCompletionWindow.setPosition(getX(), getBottom());
340 autoCompletionWindow.setSize(getWidth(), autoCompletionHeight);
343 private int computeInnerWidth() {
345 Font font = textRenderer.getFont();
347 return font.computeTextWidth("X")*columns;
353 private int computeInnerHeight() {
354 Font font = textRenderer.getFont();
356 return font.getLineHeight();
362 public int getMinWidth() {
363 int minWidth = super.getMinWidth();
364 minWidth = Math.max(minWidth, computeInnerWidth() + getBorderHorizontal());
369 public int getMinHeight() {
370 int minHeight = super.getMinHeight();
371 minHeight = Math.max(minHeight, computeInnerHeight() + getBorderVertical());
376 public int getPreferredInnerWidth() {
377 return computeInnerWidth();
381 public int getPreferredInnerHeight() {
382 return computeInnerHeight();
385 public void setErrorMessage(Object errorMsg) {
386 getAnimationState().setAnimationState(STATE_ERROR, errorMsg != null);
387 if(this.errorMsg != errorMsg) {
388 this.errorMsg = errorMsg;
391 gui.requestToolTipUpdate(this);
394 if(errorMsg != null) {
395 if(hasKeyboardFocus()) {
396 openErrorInfoWindow();
398 } else if(errorInfoWindow != null) {
399 errorInfoWindow.closeInfo();
404 public Object getTooltipContent() {
405 if(errorMsg != null) {
408 Object tooltip = super.getTooltipContent();
409 if(tooltip == null && !isPasswordMasking() && textLongerThenWidget) {
415 public void setAutoCompletionWindow(InfoWindow window, boolean wantKeys) {
417 if(autoCompletionWindow != null) {
418 autoCompletionWindow.closeInfo();
419 autoCompletionWindow = null;
421 autoCompletionWantKeys = false;
423 autoCompletionWindow = window;
424 autoCompletionWantKeys = wantKeys;
425 if(autoCompletionWindow.openInfo()) {
426 layoutAutocompletionWindow();
432 public boolean handleEvent(Event evt) {
433 boolean selectPressed = (evt.getModifiers() & Event.MODIFIER_SHIFT) != 0;
435 if(evt.isMouseEvent()) {
436 boolean hover = (evt.getType() != Event.Type.MOUSE_EXITED) && isMouseInside(evt);
437 getAnimationState().setAnimationState(STATE_HOVER, hover);
440 if(evt.isMouseDragEvent()) {
441 if(evt.getType() == Event.Type.MOUSE_DRAGED &&
442 (evt.getModifiers() & Event.MODIFIER_LBUTTON) != 0) {
443 int newPos = textRenderer.getCursorPosFromMouse(evt.getMouseX());
444 setCursorPos(newPos, true);
449 if(super.handleEvent(evt)) {
453 if(evt.isKeyEvent() && autoCompletionWantKeys) {
454 if(autoCompletionWindow.handleEvent(evt)) {
459 switch (evt.getType()) {
461 switch (evt.getKeyCode()) {
462 case Keyboard.KEY_BACK:
465 case Keyboard.KEY_DELETE:
468 case Keyboard.KEY_RETURN:
469 case Keyboard.KEY_ESCAPE:
470 doCallback(evt.getKeyCode());
472 case Keyboard.KEY_HOME:
473 setCursorPos(0, selectPressed);
475 case Keyboard.KEY_END:
476 setCursorPos(editBuffer.length(), selectPressed);
478 case Keyboard.KEY_LEFT:
479 moveCursor(-1, selectPressed);
481 case Keyboard.KEY_RIGHT:
482 moveCursor(+1, selectPressed);
484 case Keyboard.KEY_UP:
485 case Keyboard.KEY_DOWN:
486 if(!autoCompletionWantKeys && autoCompletionWindow != null) {
487 return autoCompletionWindow.handleEvent(evt);
491 if(evt.hasKeyChar()) {
492 insertChar(evt.getKeyChar());
499 switch (evt.getKeyCode()) {
500 case Keyboard.KEY_BACK:
501 case Keyboard.KEY_DELETE:
502 case Keyboard.KEY_RETURN:
503 case Keyboard.KEY_ESCAPE:
504 case Keyboard.KEY_HOME:
505 case Keyboard.KEY_END:
506 case Keyboard.KEY_LEFT:
507 case Keyboard.KEY_RIGHT:
509 case Keyboard.KEY_UP:
510 case Keyboard.KEY_DOWN:
511 if(!autoCompletionWantKeys && autoCompletionWindow != null) {
512 return autoCompletionWindow.handleEvent(evt);
516 return evt.hasKeyChar();
520 if(evt.getMouseButton() == Event.MOUSE_RBUTTON && isMouseInside(evt)) {
527 if(evt.getMouseButton() == Event.MOUSE_LBUTTON && isMouseInside(evt)) {
528 int newPos = textRenderer.getCursorPosFromMouse(evt.getMouseX());
529 setCursorPos(newPos, selectPressed);
535 if(evt.getMouseClickCount() == 2) {
536 int newPos = textRenderer.getCursorPosFromMouse(evt.getMouseX());
537 selectWordFromMouse(newPos);
540 if(evt.getMouseClickCount() == 3) {
550 return evt.isMouseEvent();
553 protected void showPopupMenu(Event evt) {
554 if(popupMenu == null) {
555 popupMenu = createPopupMenu();
557 if(popupMenu != null) {
558 popupMenu.showPopup(evt.getMouseX(), evt.getMouseY());
562 protected PopupMenu createPopupMenu() {
563 Button btnCut = new Button("cut");
564 btnCut.addCallback(new Runnable() {
570 Button btnCopy = new Button("copy");
571 btnCopy.addCallback(new Runnable() {
577 Button btnPaste = new Button("paste");
578 btnPaste.addCallback(new Runnable() {
580 pasteFromClipboard();
584 Button btnClear = new Button("clear");
585 btnClear.addCallback(new Runnable() {
593 PopupMenu menu = new PopupMenu(this);
602 private void updateText() {
604 model.setValue(getText());
606 textRenderer.setCharSequence(passwordMasking != null ? passwordMasking : editBuffer);
608 scrollToCursor(false);
609 doCallback(Keyboard.KEY_NONE);
612 private void checkTextWidth() {
613 textLongerThenWidget = textRenderer.getPreferredWidth() > textRenderer.getWidth();
616 protected void moveCursor(int dir, boolean select) {
617 setCursorPos(cursorPos + dir, select);
620 protected void setCursorPos(int pos, boolean select) {
621 pos = Math.max(0, Math.min(editBuffer.length(), pos));
623 selectionStart = pos;
626 if(this.cursorPos != pos) {
629 if(cursorPos == selectionStart) {
630 selectionStart = pos;
635 selectionStart = cursorPos;
638 if(selectionStart > selectionEnd) {
639 int t = selectionStart;
640 selectionStart = selectionEnd;
645 this.cursorPos = pos;
646 scrollToCursor(false);
650 public void selectAll() {
652 selectionEnd = editBuffer.length();
655 protected void selectWordFromMouse(int index) {
656 selectionStart = index;
657 selectionEnd = index;
658 while(selectionStart > 0 && !Character.isWhitespace(editBuffer.charAt(selectionStart-1))) {
661 while(selectionEnd < editBuffer.length() && !Character.isWhitespace(editBuffer.charAt(selectionEnd))) {
666 protected void scrollToCursor(boolean force) {
667 int xpos = textRenderer.computeRelativeCursorPositionX(cursorPos);
668 int renderWidth = textRenderer.getWidth() - 5;
669 if(xpos < scrollPos + 5) {
670 scrollPos = Math.max(0, xpos - 5);
671 } else if(force || xpos - scrollPos > renderWidth) {
672 scrollPos = Math.max(0, xpos - renderWidth);
676 protected void insertChar(char ch) {
677 // don't add control characters
678 if(!readOnly && !Character.isISOControl(ch)) {
679 boolean update = false;
684 if(editBuffer.length() < maxTextLength) {
685 editBuffer.insert(cursorPos, ch);
695 protected void deletePrev() {
700 } else if(cursorPos > 0) {
707 protected void deleteNext() {
712 } else if(cursorPos < editBuffer.length()) {
713 editBuffer.deleteCharAt(cursorPos);
719 protected void deleteSelection() {
720 editBuffer.delete(selectionStart, selectionEnd);
721 selectionEnd = selectionStart;
722 setCursorPos(selectionStart, false);
725 protected void modelChanged() {
726 String modelText = model.getValue();
727 if(editBuffer.length() != modelText.length() || !getText().equals(modelText)) {
732 protected boolean hasFocusOrPopup() {
733 return hasKeyboardFocus() || hasOpenPopups();
737 protected void paintOverlay(GUI gui) {
738 if(cursorImage != null && hasFocusOrPopup()) {
739 int xpos = textRenderer.lastTextX + textRenderer.computeRelativeCursorPositionX(cursorPos);
740 cursorImage.draw(getAnimationState(), xpos, textRenderer.computeTextY(),
741 cursorImage.getWidth(), textRenderer.getFont().getLineHeight());
743 super.paintOverlay(gui);
746 private void openErrorInfoWindow() {
747 if(autoCompletionWindow == null || !autoCompletionWindow.isOpen()) {
748 if(errorInfoWindow == null) {
749 errorInfoLabel = new Label();
750 errorInfoWindow = new InfoWindow(this);
751 errorInfoWindow.setTheme("editfield-errorinfowindow");
752 errorInfoWindow.add(errorInfoLabel);
754 errorInfoLabel.setText(errorMsg.toString());
755 errorInfoWindow.openInfo();
756 layoutErrorInfoWindow();
760 private void layoutErrorInfoWindow() {
761 errorInfoWindow.setSize(getWidth(), errorInfoWindow.getPreferredHeight());
762 errorInfoWindow.setPosition(getX(), getBottom());
766 protected void keyboardFocusGained() {
767 if(errorMsg != null) {
768 openErrorInfoWindow();
773 protected void keyboardFocusLost() {
774 super.keyboardFocusLost();
775 if(errorInfoWindow != null) {
776 errorInfoWindow.closeInfo();
780 protected class ModelChangeListener implements Runnable {
786 protected class TextRenderer extends TextWidget {
789 protected TextRenderer(AnimationState animState) {
794 protected void paintWidget(GUI gui) {
795 lastTextX = computeTextX();
796 if(hasSelection() && hasFocusOrPopup()) {
797 if(selectionImage != null) {
798 int xpos0 = lastTextX + computeRelativeCursorPositionX(selectionStart);
799 int xpos1 = lastTextX + computeRelativeCursorPositionX(selectionEnd);
800 selectionImage.draw(getAnimationState(), xpos0, computeTextY(),
801 xpos1 - xpos0, getFont().getLineHeight());
803 paintWithSelection(getAnimationState(), selectionStart, selectionEnd);
805 paintLabelText(getAnimationState());
810 protected void sizeChanged() {
811 scrollToCursor(true);
815 protected int computeTextX() {
816 if(hasFocusOrPopup()) {
817 return getInnerX() - scrollPos;
823 protected int getCursorPosFromMouse(int x) {
824 if(getFont() != null) {
825 x += getFont().getSpaceWidth() / 2;
826 return getFont().computeVisibleGlpyhs(
827 getText(), 0, editBuffer.length(),
835 static class PasswordMasker implements CharSequence {
836 private final CharSequence base;
837 private final char maskingChar;
839 public PasswordMasker(CharSequence base, char maskingChar) {
841 this.maskingChar = maskingChar;
844 public int length() {
845 return base.length();
848 public char charAt(int index) {
852 public CharSequence subSequence(int start, int end) {
853 throw new UnsupportedOperationException("Not supported.");