src/de/matthiasmann/twl/EditField.java
author Matthias Mann
Mon Mar 01 18:37:39 2010 +0100 (23 months ago)
changeset 431 cefe1c8066a5
parent 387 bc41d0745c99
child 432 ece24ea8c1d4
permissions -rw-r--r--
added alt/menu key support to input maps
     1 /*
     2  * Copyright (c) 2008-2010, Matthias Mann
     3  * 
     4  * All rights reserved.
     5  * 
     6  * Redistribution and use in source and binary forms, with or without
     7  * modification, are permitted provided that the following conditions are met:
     8  * 
     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.
    17  * 
    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.
    29  */
    30 package de.matthiasmann.twl;
    31 
    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;
    38 
    39 /**
    40  * A simple one line edit field
    41  * 
    42  * @author Matthias Mann
    43  */
    44 public class EditField extends Widget {
    45 
    46     public static final String STATE_ERROR = "error";
    47     public static final String STATE_READONLY = "readonly";
    48     public static final String STATE_HOVER = "hover";
    49     
    50     public interface Callback {
    51         /**
    52          * Gets called for any change in the edit field, or when ESCAPE or RETURN was pressed
    53          *
    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
    58          */
    59         public void callback(int key);
    60     }
    61 
    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;
    68 
    69     private int cursorPos;
    70     private int scrollPos;
    71     private int selectionStart;
    72     private int selectionEnd;
    73     private int maxTextLength = Short.MAX_VALUE;
    74 
    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;
    83 
    84     private InfoWindow autoCompletionWindow;
    85     private boolean autoCompletionWantKeys;
    86     private int autoCompletionHeight = 100;
    87 
    88     private InfoWindow errorInfoWindow;
    89     private Label errorInfoLabel;
    90 
    91     /**
    92      * Creates a new EditField with an optional parent animation state.
    93      *
    94      * Unlike other widgets which use the passed animation state directly,
    95      * the EditField always creates it's animation state with the passed
    96      * one as parent.
    97      *
    98      * @param parentAnimationState
    99      * @see AnimationState#AnimationState(de.matthiasmann.twl.AnimationState) 
   100      */
   101     public EditField(AnimationState parentAnimationState) {
   102         super(parentAnimationState, true);
   103         
   104         this.editBuffer = new StringBuilder();
   105         this.textRenderer = new TextRenderer(getAnimationState());
   106         this.passwordChar = '*';
   107 
   108         textRenderer.setTheme("renderer");
   109         textRenderer.setClip(true);
   110         
   111         add(textRenderer);
   112         setCanAcceptKeyboardFocus(true);
   113         setDepthFocusTraversal(false);
   114 
   115         addActionMapping("cut", "cutToClipboard");
   116         addActionMapping("copy", "copyToClipboard");
   117         addActionMapping("paste", "pasteFromClipboard");
   118         addActionMapping("selectAll", "selectAll");
   119     }
   120 
   121     public EditField() {
   122         this(null);
   123     }
   124 
   125     public void addCallback(Callback cb) {
   126         callbacks = CallbackSupport.addCallbackToList(callbacks, cb, Callback.class);
   127     }
   128 
   129     public void removeCallback(Callback cb) {
   130         callbacks = CallbackSupport.removeCallbackFromList(callbacks, cb);
   131     }
   132 
   133     protected void doCallback(int key) {
   134         if(callbacks != null) {
   135             for(Callback cb : callbacks) {
   136                 cb.callback(key);
   137             }
   138         }
   139     }
   140 
   141     public boolean isPasswordMasking() {
   142         return passwordMasking != null;
   143     }
   144 
   145     public void setPasswordMasking(boolean passwordMasking) {
   146         if(passwordMasking != isPasswordMasking()) {
   147             if(passwordMasking) {
   148                 this.passwordMasking = new PasswordMasker(editBuffer, passwordChar);
   149             } else {
   150                 this.passwordMasking = null;
   151             }
   152             updateText();
   153         }
   154     }
   155 
   156     public char getPasswordChar() {
   157         return passwordChar;
   158     }
   159 
   160     public void setPasswordChar(char passwordChar) {
   161         this.passwordChar = passwordChar;
   162         if(passwordMasking != null) {
   163             passwordMasking = new PasswordMasker(editBuffer, passwordChar);
   164             updateText();
   165         }
   166     }
   167 
   168     public int getColumns() {
   169         return columns;
   170     }
   171 
   172     /**
   173      * This is used to determine the desired width of the EditField based on
   174      * it's font and the character 'X'
   175      * 
   176      * @param columns number of characters
   177      * @throws IllegalArgumentException if columns < 0
   178      */
   179     public void setColumns(int columns) {
   180         if(columns < 0) {
   181             throw new IllegalArgumentException("columns");
   182         }
   183         this.columns = columns;
   184     }
   185 
   186     public StringModel getModel() {
   187         return model;
   188     }
   189 
   190     public void setModel(StringModel model) {
   191         if(this.model != null) {
   192             this.model.removeCallback(modelChangeListener);
   193         }
   194         this.model = model;
   195         if(this.model != null) {
   196             if(modelChangeListener == null) {
   197                 modelChangeListener = new ModelChangeListener();
   198             }
   199             this.model.addCallback(modelChangeListener);
   200             modelChanged();
   201         }
   202     }
   203 
   204     public void setText(String text) {
   205         text = TextUtil.limitStringLength(text, maxTextLength);
   206         editBuffer.replace(0, editBuffer.length(), text);
   207         cursorPos = editBuffer.length();
   208         selectionStart = 0;
   209         selectionEnd = 0;
   210         updateText();
   211         scrollToCursor(true);
   212     }
   213 
   214     public String getText() {
   215         return editBuffer.toString();
   216     }
   217     
   218     public String getSelectedText() {
   219         return editBuffer.substring(selectionStart, selectionEnd);
   220     }
   221 
   222     public boolean hasSelection() {
   223         return selectionStart != selectionEnd;
   224     }
   225 
   226     public int getCursorPos() {
   227         return cursorPos;
   228     }
   229 
   230     public int getTextLength() {
   231         return editBuffer.length();
   232     }
   233     
   234     public boolean isReadOnly() {
   235         return readOnly;
   236     }
   237 
   238     public void setReadOnly(boolean readOnly) {
   239         this.readOnly = readOnly;
   240         getAnimationState().setAnimationState(STATE_READONLY, readOnly);
   241     }
   242 
   243     public void insertText(String str) {
   244         if(!readOnly) {
   245             boolean update = false;
   246             if(hasSelection()) {
   247                 deleteSelection();
   248                 update = true;
   249             }
   250             int insertLength = Math.min(str.length(), maxTextLength - editBuffer.length());
   251             if(insertLength > 0) {
   252                 editBuffer.insert(cursorPos, str, 0, insertLength);
   253                 cursorPos += insertLength;
   254                 update = true;
   255             }
   256             if(update) {
   257                 updateText();
   258             }
   259         }
   260     }
   261 
   262     public void pasteFromClipboard() {
   263         String cbText = Clipboard.getClipboard();
   264         if(cbText != null) {
   265             cbText = TextUtil.stripNewLines(cbText);
   266             insertText(cbText);
   267         }
   268     }
   269 
   270     public void copyToClipboard() {
   271         String text;
   272         if(hasSelection()) {
   273             text = getSelectedText();
   274         } else {
   275             text = getText();
   276         }
   277         if(isPasswordMasking()) {
   278             text = TextUtil.createString(passwordChar, text.length());
   279         }
   280         Clipboard.setClipboard(text);
   281     }
   282 
   283     public void cutToClipboard() {
   284         String text;
   285         if(hasSelection()) {
   286             text = getSelectedText();
   287             if(!readOnly) {
   288                 deleteSelection();
   289                 updateText();
   290             }
   291         } else {
   292             text = getText();
   293             if(!readOnly) {
   294                 setText("");
   295             }
   296         }
   297         if(isPasswordMasking()) {
   298             text = TextUtil.createString(passwordChar, text.length());
   299         }
   300         Clipboard.setClipboard(text);
   301     }
   302 
   303     public int getMaxTextLength() {
   304         return maxTextLength;
   305     }
   306 
   307     public void setMaxTextLength(int maxTextLength) {
   308         this.maxTextLength = maxTextLength;
   309     }
   310 
   311     @Override
   312     protected void applyTheme(ThemeInfo themeInfo) {
   313         super.applyTheme(themeInfo);
   314         applayThemeEditField(themeInfo);
   315     }
   316 
   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
   324     }
   325 
   326     @Override
   327     protected void layout() {
   328         layoutChildFullInnerArea(textRenderer);
   329         checkTextWidth();
   330         if(autoCompletionWindow != null) {
   331             layoutAutocompletionWindow();
   332         }
   333         if(errorInfoWindow != null) {
   334             layoutErrorInfoWindow();
   335         }
   336     }
   337 
   338     private void layoutAutocompletionWindow() {
   339         autoCompletionWindow.setPosition(getX(), getBottom());
   340         autoCompletionWindow.setSize(getWidth(), autoCompletionHeight);
   341     }
   342 
   343     private int computeInnerWidth() {
   344         if(columns > 0) {
   345             Font font = textRenderer.getFont();
   346             if(font != null) {
   347                 return font.computeTextWidth("X")*columns;
   348             }
   349         }
   350         return 0;
   351     }
   352 
   353     private int computeInnerHeight() {
   354         Font font = textRenderer.getFont();
   355         if(font != null) {
   356             return font.getLineHeight();
   357         }
   358         return 0;
   359     }
   360 
   361     @Override
   362     public int getMinWidth() {
   363         int minWidth = super.getMinWidth();
   364         minWidth = Math.max(minWidth, computeInnerWidth() + getBorderHorizontal());
   365         return minWidth;
   366     }
   367 
   368     @Override
   369     public int getMinHeight() {
   370         int minHeight = super.getMinHeight();
   371         minHeight = Math.max(minHeight, computeInnerHeight() + getBorderVertical());
   372         return minHeight;
   373     }
   374 
   375     @Override
   376     public int getPreferredInnerWidth() {
   377         return computeInnerWidth();
   378     }
   379 
   380     @Override
   381     public int getPreferredInnerHeight() {
   382         return computeInnerHeight();
   383     }
   384 
   385     public void setErrorMessage(Object errorMsg) {
   386         getAnimationState().setAnimationState(STATE_ERROR, errorMsg != null);
   387         if(this.errorMsg != errorMsg) {
   388             this.errorMsg = errorMsg;
   389             GUI gui = getGUI();
   390             if(gui != null) {
   391                 gui.requestToolTipUpdate(this);
   392             }
   393         }
   394         if(errorMsg != null) {
   395             if(hasKeyboardFocus()) {
   396                 openErrorInfoWindow();
   397             }
   398         } else if(errorInfoWindow != null) {
   399             errorInfoWindow.closeInfo();
   400         }
   401     }
   402 
   403     @Override
   404     public Object getTooltipContent() {
   405         if(errorMsg != null) {
   406             return errorMsg;
   407         }
   408         Object tooltip = super.getTooltipContent();
   409         if(tooltip == null && !isPasswordMasking() && textLongerThenWidget) {
   410             tooltip = getText();
   411         }
   412         return tooltip;
   413     }
   414 
   415     public void setAutoCompletionWindow(InfoWindow window, boolean wantKeys) {
   416         if(window == null) {
   417             if(autoCompletionWindow != null) {
   418                 autoCompletionWindow.closeInfo();
   419                 autoCompletionWindow = null;
   420             }
   421             autoCompletionWantKeys = false;
   422         } else {
   423             autoCompletionWindow = window;
   424             autoCompletionWantKeys = wantKeys;
   425             if(autoCompletionWindow.openInfo()) {
   426                 layoutAutocompletionWindow();
   427             }
   428         }
   429     }
   430 
   431     @Override
   432     public boolean handleEvent(Event evt) {
   433         boolean selectPressed = (evt.getModifiers() & Event.MODIFIER_SHIFT) != 0;
   434 
   435         if(evt.isMouseEvent()) {
   436             boolean hover = (evt.getType() != Event.Type.MOUSE_EXITED) && isMouseInside(evt);
   437             getAnimationState().setAnimationState(STATE_HOVER, hover);
   438         }
   439 
   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);
   445             }
   446             return true;
   447         }
   448 
   449         if(super.handleEvent(evt)) {
   450             return true;
   451         }
   452 
   453         if(evt.isKeyEvent() && autoCompletionWantKeys) {
   454             if(autoCompletionWindow.handleEvent(evt)) {
   455                 return true;
   456             }
   457         }
   458         
   459         switch (evt.getType()) {
   460         case KEY_PRESSED:
   461             switch (evt.getKeyCode()) {
   462             case Keyboard.KEY_BACK:
   463                 deletePrev();
   464                 return true;
   465             case Keyboard.KEY_DELETE:
   466                 deleteNext();
   467                 return true;
   468             case Keyboard.KEY_RETURN:
   469             case Keyboard.KEY_ESCAPE:
   470                 doCallback(evt.getKeyCode());
   471                 return true;
   472             case Keyboard.KEY_HOME:
   473                 setCursorPos(0, selectPressed);
   474                 return true;
   475             case Keyboard.KEY_END:
   476                 setCursorPos(editBuffer.length(), selectPressed);
   477                 return true;
   478             case Keyboard.KEY_LEFT:
   479                 moveCursor(-1, selectPressed);
   480                 return true;
   481             case Keyboard.KEY_RIGHT:
   482                 moveCursor(+1, selectPressed);
   483                 return true;
   484             case Keyboard.KEY_UP:
   485             case Keyboard.KEY_DOWN:
   486                 if(!autoCompletionWantKeys && autoCompletionWindow != null) {
   487                     return autoCompletionWindow.handleEvent(evt);
   488                 }
   489                 return false;
   490             default:
   491                 if(evt.hasKeyChar()) {
   492                     insertChar(evt.getKeyChar());
   493                     return true;
   494                 }
   495                 return false;
   496             }
   497 
   498         case KEY_RELEASED:
   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:
   508                 return true;
   509             case Keyboard.KEY_UP:
   510             case Keyboard.KEY_DOWN:
   511                 if(!autoCompletionWantKeys && autoCompletionWindow != null) {
   512                     return autoCompletionWindow.handleEvent(evt);
   513                 }
   514                 return false;
   515             default:
   516                 return evt.hasKeyChar();
   517             }
   518 
   519         case MOUSE_BTNUP:
   520             if(evt.getMouseButton() == Event.MOUSE_RBUTTON && isMouseInside(evt)) {
   521                 showPopupMenu(evt);
   522                 return true;
   523             }
   524             break;
   525 
   526         case MOUSE_BTNDOWN:
   527             if(evt.getMouseButton() == Event.MOUSE_LBUTTON && isMouseInside(evt)) {
   528                 int newPos = textRenderer.getCursorPosFromMouse(evt.getMouseX());
   529                 setCursorPos(newPos, selectPressed);
   530                 return true;
   531             }
   532             break;
   533 
   534         case MOUSE_CLICKED:
   535             if(evt.getMouseClickCount() == 2) {
   536                 int newPos = textRenderer.getCursorPosFromMouse(evt.getMouseX());
   537                 selectWordFromMouse(newPos);
   538                 return true;
   539             }
   540             if(evt.getMouseClickCount() == 3) {
   541                 selectAll();
   542                 return true;
   543             }
   544             break;
   545 
   546         case MOUSE_WHEEL:
   547             return false;
   548         }
   549 
   550         return evt.isMouseEvent();
   551     }
   552 
   553     protected void showPopupMenu(Event evt) {
   554         if(popupMenu == null) {
   555             popupMenu = createPopupMenu();
   556         }
   557         if(popupMenu != null) {
   558             popupMenu.showPopup(evt.getMouseX(), evt.getMouseY());
   559         }
   560     }
   561 
   562     protected PopupMenu createPopupMenu() {
   563         Button btnCut = new Button("cut");
   564         btnCut.addCallback(new Runnable() {
   565             public void run() {
   566                 cutToClipboard();
   567             }
   568         });
   569 
   570         Button btnCopy = new Button("copy");
   571         btnCopy.addCallback(new Runnable() {
   572             public void run() {
   573                 copyToClipboard();
   574             }
   575         });
   576 
   577         Button btnPaste = new Button("paste");
   578         btnPaste.addCallback(new Runnable() {
   579             public void run() {
   580                 pasteFromClipboard();
   581             }
   582         });
   583 
   584         Button btnClear = new Button("clear");
   585         btnClear.addCallback(new Runnable() {
   586             public void run() {
   587                 if(!isReadOnly()) {
   588                     setText("");
   589                 }
   590             }
   591         });
   592 
   593         PopupMenu menu = new PopupMenu(this);
   594         menu.add(btnCut);
   595         menu.add(btnCopy);
   596         menu.add(btnPaste);
   597         menu.addSpacer();
   598         menu.add(btnClear);
   599         return menu;
   600     }
   601 
   602     private void updateText() {
   603         if(model != null) {
   604             model.setValue(getText());
   605         }
   606         textRenderer.setCharSequence(passwordMasking != null ? passwordMasking : editBuffer);
   607         checkTextWidth();
   608         scrollToCursor(false);
   609         doCallback(Keyboard.KEY_NONE);
   610     }
   611 
   612     private void checkTextWidth() {
   613         textLongerThenWidget = textRenderer.getPreferredWidth() > textRenderer.getWidth();
   614     }
   615 
   616     protected void moveCursor(int dir, boolean select) {
   617         setCursorPos(cursorPos + dir, select);
   618     }
   619 
   620     protected void setCursorPos(int pos, boolean select) {
   621         pos = Math.max(0, Math.min(editBuffer.length(), pos));
   622         if(!select) {
   623             selectionStart = pos;
   624             selectionEnd = pos;
   625         }
   626         if(this.cursorPos != pos) {
   627             if(select) {
   628                 if(hasSelection()) {
   629                     if(cursorPos == selectionStart) {
   630                         selectionStart = pos;
   631                     } else {
   632                         selectionEnd = pos;
   633                     }
   634                 } else {
   635                     selectionStart = cursorPos;
   636                     selectionEnd = pos;
   637                 }
   638                 if(selectionStart > selectionEnd) {
   639                     int t = selectionStart;
   640                     selectionStart = selectionEnd;
   641                     selectionEnd = t;
   642                 }
   643             }
   644 
   645             this.cursorPos = pos;
   646             scrollToCursor(false);
   647         }
   648     }
   649 
   650     public void selectAll() {
   651         selectionStart = 0;
   652         selectionEnd = editBuffer.length();
   653     }
   654 
   655     protected void selectWordFromMouse(int index) {
   656         selectionStart = index;
   657         selectionEnd = index;
   658         while(selectionStart > 0 && !Character.isWhitespace(editBuffer.charAt(selectionStart-1))) {
   659             selectionStart--;
   660         }
   661         while(selectionEnd < editBuffer.length() && !Character.isWhitespace(editBuffer.charAt(selectionEnd))) {
   662             selectionEnd++;
   663         }
   664     }
   665 
   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);
   673         }
   674     }
   675     
   676     protected void insertChar(char ch) {
   677         // don't add control characters
   678         if(!readOnly && !Character.isISOControl(ch)) {
   679             boolean update = false;
   680             if(hasSelection()) {
   681                 deleteSelection();
   682                 update = true;
   683             }
   684             if(editBuffer.length() < maxTextLength) {
   685                 editBuffer.insert(cursorPos, ch);
   686                 cursorPos++;
   687                 update = true;
   688             }
   689             if(update) {
   690                 updateText();
   691             }
   692         }
   693     }
   694 
   695     protected void deletePrev() {
   696         if(!readOnly) {
   697             if(hasSelection()) {
   698                 deleteSelection();
   699                 updateText();
   700             } else if(cursorPos > 0) {
   701                 --cursorPos;
   702                 deleteNext();
   703             }
   704         }
   705     }
   706 
   707     protected void deleteNext() {
   708         if(!readOnly) {
   709             if(hasSelection()) {
   710                 deleteSelection();
   711                 updateText();
   712             } else if(cursorPos < editBuffer.length()) {
   713                 editBuffer.deleteCharAt(cursorPos);
   714                 updateText();
   715             }
   716         }
   717     }
   718 
   719     protected void deleteSelection() {
   720         editBuffer.delete(selectionStart, selectionEnd);
   721         selectionEnd = selectionStart;
   722         setCursorPos(selectionStart, false);
   723     }
   724 
   725     protected void modelChanged() {
   726         String modelText = model.getValue();
   727         if(editBuffer.length() != modelText.length() || !getText().equals(modelText)) {
   728             setText(modelText);
   729         }
   730     }
   731 
   732     protected boolean hasFocusOrPopup() {
   733         return hasKeyboardFocus() || hasOpenPopups();
   734     }
   735     
   736     @Override
   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());
   742         }
   743         super.paintOverlay(gui);
   744     }
   745 
   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);
   753             }
   754             errorInfoLabel.setText(errorMsg.toString());
   755             errorInfoWindow.openInfo();
   756             layoutErrorInfoWindow();
   757         }
   758     }
   759 
   760     private void layoutErrorInfoWindow() {
   761         errorInfoWindow.setSize(getWidth(), errorInfoWindow.getPreferredHeight());
   762         errorInfoWindow.setPosition(getX(), getBottom());
   763     }
   764 
   765     @Override
   766     protected void keyboardFocusGained() {
   767         if(errorMsg != null) {
   768             openErrorInfoWindow();
   769         }
   770     }
   771 
   772     @Override
   773     protected void keyboardFocusLost() {
   774         super.keyboardFocusLost();
   775         if(errorInfoWindow != null) {
   776             errorInfoWindow.closeInfo();
   777         }
   778     }
   779 
   780     protected class ModelChangeListener implements Runnable {
   781         public void run() {
   782             modelChanged();
   783         }
   784     }
   785 
   786     protected class TextRenderer extends TextWidget {
   787         int lastTextX;
   788 
   789         protected TextRenderer(AnimationState animState) {
   790             super(animState);
   791         }
   792 
   793         @Override
   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());
   802                 }
   803                 paintWithSelection(getAnimationState(), selectionStart, selectionEnd);
   804             } else {
   805                 paintLabelText(getAnimationState());
   806             }
   807         }
   808 
   809         @Override
   810         protected void sizeChanged() {
   811             scrollToCursor(true);
   812         }
   813 
   814         @Override
   815         protected int computeTextX() {
   816             if(hasFocusOrPopup()) {
   817                 return getInnerX() - scrollPos;
   818             } else {
   819                 return getInnerX();
   820             }
   821         }
   822 
   823         protected int getCursorPosFromMouse(int x) {
   824             if(getFont() != null) {
   825                 x += getFont().getSpaceWidth() / 2;
   826                 return getFont().computeVisibleGlpyhs(
   827                         getText(), 0, editBuffer.length(),
   828                         x - lastTextX);
   829             } else {
   830                 return 0;
   831             }
   832         }
   833     }
   834 
   835     static class PasswordMasker implements CharSequence {
   836         private final CharSequence base;
   837         private final char maskingChar;
   838 
   839         public PasswordMasker(CharSequence base, char maskingChar) {
   840             this.base = base;
   841             this.maskingChar = maskingChar;
   842         }
   843 
   844         public int length() {
   845             return base.length();
   846         }
   847 
   848         public char charAt(int index) {
   849             return maskingChar;
   850         }
   851 
   852         public CharSequence subSequence(int start, int end) {
   853             throw new UnsupportedOperationException("Not supported.");
   854         }
   855     }
   856 
   857 }