/*********************************************************************** * mt4j Copyright (c) 2008 - 2009, C.Ruff, Fraunhofer-Gesellschaft All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * ***********************************************************************/ package org.mt4j.components.visibleComponents.widgets; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.media.opengl.GL; import javax.media.opengl.glu.GLU; import org.mt4j.MTApplication; import org.mt4j.components.TransformSpace; import org.mt4j.components.clipping.Clip; import org.mt4j.components.visibleComponents.font.BitmapFont; import org.mt4j.components.visibleComponents.font.BitmapFontCharacter; import org.mt4j.components.visibleComponents.font.FontManager; import org.mt4j.components.visibleComponents.font.IFont; import org.mt4j.components.visibleComponents.font.IFontCharacter; import org.mt4j.components.visibleComponents.shapes.MTRectangle; import org.mt4j.components.visibleComponents.widgets.keyboard.ITextInputListener; import org.mt4j.components.visibleComponents.widgets.keyboard.MTKeyboard; import org.mt4j.input.inputProcessors.componentProcessors.lassoProcessor.IdragClusterable; import org.mt4j.util.MT4jSettings; import org.mt4j.util.MTColor; import org.mt4j.util.math.Matrix; import org.mt4j.util.math.Tools3D; import org.mt4j.util.math.Vector3D; import org.mt4j.util.math.Vertex; import processing.core.PApplet; import processing.core.PGraphics; /** * The Class MTTextArea. This widget allows to display text with a specified font. * If the constructor with no fixed text are dimensions is used, the text area will * expand itself to fit the text in. * <br> * If the constructor with fixed dimensions is used, the text will have word wrapping * and be clipped to the specified dimensions. * * @author Christopher Ruff */ public class MTTextArea extends MTRectangle implements IdragClusterable, ITextInputListener, Comparable<Object>{ //Standard expand direction is {@link ExpandDirection#UP} for // backward compatibility. /** * Determines the vertical expand direction of the {@link MTTextArea} * if the text area will expand itself to fit the text in. * * */ public enum ExpandDirection { /** Expand the {@link MTTextArea} in top direction if necessary. */ UP, /** Expand the {@link MTTextArea} in bottom direction if necassary. */ DOWN; } /** The pa. */ private PApplet pa; /** The character list. */ private ArrayList<IFontCharacter> characterList; /** The font. */ private IFont font; /** The font b box height. */ private int fontHeight; /** The show caret. */ private boolean showCaret; /** The show caret time. */ private long showCaretTime = 1500; //ms /** The caret time counter. */ private int caretTimeCounter = 0; /** The enable caret. */ private boolean enableCaret; /** The caret width. */ private float caretWidth; private int innerPaddingTop; private int innerPaddingLeft; private float totalScrollTextX; private float totalScrollTextY; //TODO set font color on the fly //TODO different font sizes in one textarea? //TODO (create mode : expand vertically but do word wrap horizontally?) private static final int MODE_EXPAND = 0; private static final int MODE_WRAP = 1; private int mode; private static ArtificalLineBreak artificialLineBreak; private ExpandDirection expandDirection ; /** * Instantiates a new text area. This constructor creates * a text area with variable dimensions that expands itself when text is added. * A default font is used. * * @param pApplet the applet */ public MTTextArea(PApplet pApplet) { this(pApplet, FontManager.getInstance().getDefaultFont(pApplet)); } /** * Instantiates a new text area. This constructor creates * a text area with variable dimensions that expands itself when text is added. * * @param pApplet the applet * @param font the font */ public MTTextArea(PApplet pApplet, IFont font) { super( 0, 0, //upper left corner 0, //width 0, //height pApplet); init(pApplet, font, MODE_EXPAND); //Position textarea at 0,0 this.setUpperLeftPos(Vector3D.ZERO_VECTOR); //Expand vertically at enter this.setHeightLocal(this.getTotalLinesHeight()); this.setWidthLocal(getMaxLineWidth()); } /** * Instantiates a new mT text area. * This constructor creates a textarea with fixed dimensions. * If the text exceeds the dimensions the text is clipped. * A default font is used. * * @param x the x * @param y the y * @param width the width * @param height the height * @param pApplet the applet */ public MTTextArea(float x, float y, float width, float height, PApplet pApplet) { this(x, y, width, height, FontManager.getInstance().getDefaultFont(pApplet), pApplet); } /** * Instantiates a new mT text area. * This constructor creates a textarea with fixed dimensions. * If the text exceeds the dimensions the text is clipped. * * @param x the x * @param y the y * @param width the width * @param height the height * @param font the font * @param pApplet the applet */ public MTTextArea(float x, float y, float width, float height, IFont font, PApplet pApplet) { super( 0, 0, //upper left corner width, //width height, //height pApplet); init(pApplet, font, MODE_WRAP); //Position textarea at x,y this.setUpperLeftPos(new Vector3D(x,y,0)); } private void setUpperLeftPos(Vector3D pos){ //Position textarea at 0,0 PositionAnchor prevAnchor = this.getAnchor(); this.setAnchor(PositionAnchor.UPPER_LEFT); this.setPositionGlobal(pos); this.setAnchor(prevAnchor); } private void init(PApplet pApplet, IFont font, int mode){ this.pa = pApplet; this.font = font; this.expandDirection = ExpandDirection.DOWN; this.mode = mode; switch (this.mode) { case MODE_EXPAND: //We dont have to clip since we expand the area break; case MODE_WRAP: if (MT4jSettings.getInstance().isOpenGlMode()){ //Clip the text to the area this.setClip(new Clip(pApplet, this.getVerticesLocal()[0].x, this.getVerticesLocal()[0].y, this.getWidthXY(TransformSpace.LOCAL), this.getHeightXY(TransformSpace.LOCAL))); } break; default: break; } characterList = new ArrayList<IFontCharacter>(); if (MT4jSettings.getInstance().isOpenGlMode()) this.setUseDirectGL(true); fontHeight = font.getFontAbsoluteHeight(); caretWidth = 0; innerPaddingTop = 5; innerPaddingLeft = 8; showCaret = false; enableCaret = false; showCaretTime = 1000; this.setStrokeWeight(1.5f); this.setStrokeColor(new MTColor(255, 255, 255, 255)); this.setDrawSmooth(true); //Draw this component and its children above //everything previously drawn and avoid z-fighting this.setDepthBufferDisabled(true); this.totalScrollTextX = 0.0f; this.totalScrollTextY = 0.0f; if (artificialLineBreak == null){ artificialLineBreak = new ArtificalLineBreak(); } this.isBitmapFont = (font instanceof BitmapFont)? true : false; } /** * Sets the font. * @param font the new font */ public void setFont(IFont font){ this.font = font; this.fontHeight = font.getFontAbsoluteHeight(); this.isBitmapFont = (font instanceof BitmapFont)? true : false; this.updateLayout(); } @Override public void updateComponent(long timeDelta) { super.updateComponent(timeDelta); if (enableCaret){ caretTimeCounter+=timeDelta; if (caretTimeCounter >= showCaretTime && !showCaret){ showCaret = true; caretTimeCounter = 0; }else if (caretTimeCounter >= showCaretTime && showCaret){ showCaret = false; caretTimeCounter = 0; } } } @Override public void preDraw(PGraphics graphics) { super.preDraw(graphics); //Hack for drawing anti aliased stroke outline over the clipped area noStrokeSettingSaved = this.isNoStroke(); if (this.mode == MODE_WRAP && this.getClip() != null && !this.isNoStroke()){ this.setNoStroke(true); } } //FIXME TEST Align/round text with screen pixels private boolean textPositionRounding = true; private boolean snapVectorDirty = false; private Vector3D defaultScale = new Vector3D(1,1,1); private Vector3D globalTranslation = new Vector3D(); private Vector3D rounded = new Vector3D(); private float tolerance = 0.05f; private boolean isBitmapFont = false; private Vector3D diff = new Vector3D(0,0,0); public void setTextPositionRounding(boolean snap){ this.textPositionRounding = snap; } public boolean isTextPositionRounding(){ return this.textPositionRounding; } @Override public void setMatricesDirty(boolean baseMatrixDirty) { super.setMatricesDirty(baseMatrixDirty); if (baseMatrixDirty) snapVectorDirty = baseMatrixDirty; } private boolean useDisplayList = false; private boolean contentDisplayListDirty = true; private void setContentDisplayListDirty(boolean dirty){ this.contentDisplayListDirty = dirty; this.useDisplayList = (this.contentDisplayListDirty) ? false : true; } private int displayListID = 0; public int useContentDisplayList(){ if (enableCaret) return -1; GL gl = GLU.getCurrentGL(); //Delete old one if (this.displayListID != 0){ gl.glDeleteLists(this.displayListID, 1); } //Create new list int listIDFill = gl.glGenLists(1); if (listIDFill == 0){ System.err.println("Failed to create fill display list"); } int thisLineTotalXAdvancement = 0; int lastXAdvancement = innerPaddingLeft; //To set caret at most left start pos when charlist empty (looks better) if (enableCaret && showCaret && characterList.size() == 1){ lastXAdvancement = 0; } //Record list gl.glNewList(listIDFill, GL.GL_COMPILE); drawCharactersGL(gl, characterList, characterList.size(), lastXAdvancement, thisLineTotalXAdvancement); gl.glEndList(); if (listIDFill != 0){ useDisplayList = true; displayListID = listIDFill; } this.setContentDisplayListDirty(false); return (listIDFill == 0)? -1 : listIDFill; } @Override protected void destroyComponent() { super.destroyComponent(); if (MT4jSettings.getInstance().isOpenGlMode() && this.displayListID != 0){ GL gl = GLU.getCurrentGL(); //Delete old one if (gl != null){ gl.glDeleteLists(this.displayListID, 1); } } } @Override public void drawComponent(PGraphics g) { super.drawComponent(g); //FIXME snapping wont be useful if textarea is created at non-integer value!? and if Camera isnt default camera //if global matrix set dirty and comp not scaled -> calculate new diff vector -> apply //if snap enabled -> apply diff vector boolean applySnap = false; if (isBitmapFont && textPositionRounding){ if (snapVectorDirty){ //Calc new snap vector Matrix m = this.getGlobalMatrix(); if (m.getScale().equalsVectorWithTolerance(defaultScale, tolerance)){ //Only if no scale applied applySnap = true; globalTranslation.setXYZ(m.m03, m.m13, m.m23); rounded.setXYZ(Math.round(globalTranslation.x), Math.round(globalTranslation.y), Math.round(globalTranslation.z)); // rounded.setXYZ((int)globalTranslation.x, (int)globalTranslation.y, (int)globalTranslation.z); rounded.subtractLocal(globalTranslation); diff.setXYZ(rounded.x, rounded.y, rounded.z); snapVectorDirty = false; g.pushMatrix(); g.translate(diff.x, diff.y, diff.z); }else{ //global matrix was set dirty but the textarea is scaled -> dont apply snapvector because it gets blurry anyway if scaled // snapVectorDirty = false; //because only if scale changes back to 1,1,1 we have to calc new snapvector again applySnap = false; } }else{ //new Snap vector already calculated since global matrix was changed applySnap = true; g.pushMatrix(); g.translate(diff.x, diff.y, diff.z); } } //Add caret if its time if (enableCaret && showCaret){ characterList.add(this.getFont().getFontCharacterByUnicode("|")); } int charListSize = characterList.size(); int thisLineTotalXAdvancement = 0; int lastXAdvancement = innerPaddingLeft; //Account for TOP inner padding if using WRAP mode -> translate text switch (this.mode) { case MODE_EXPAND: //Dont need to translate for innerpadding TOP because we do that in setHeight() making the whole textarea bigger g.pushMatrix(); //FIXME TEST g.translate(0, innerPaddingTop); break; case MODE_WRAP: //Need to translate innerpadding TOP because we shouldnt make the textarea bigger like in expand mode g.pushMatrix(); g.translate(0, innerPaddingTop); break; default: break; } // /*// //To set caret at most left start pos when charlist empty (looks better) if (enableCaret && showCaret && charListSize == 1){ lastXAdvancement = 0; } // */ if (this.isUseDirectGL()){ GL gl = Tools3D.beginGL(pa); if (totalScrollTextX != 0.0f && totalScrollTextY != 0.0f){ gl.glTranslatef(totalScrollTextX, totalScrollTextY + font.getFontMaxAscent(), 0); }else{ gl.glTranslatef(0, font.getFontMaxAscent(), 0); } /* //Disabled so that no new list is created everytime something changes if (!enableCaret && useDisplayList && this.contentDisplayListDirty){ //Re-Create displaylist this.useContentDisplayList(); } */ //TODO avoid many stateChanges //in bitmap font mode for example: //enable textures, enable vertex arrays and color only once! if(!enableCaret && useDisplayList && this.displayListID != 0){ gl.glCallList(this.displayListID); }else{ drawCharactersGL(gl, characterList, charListSize, lastXAdvancement, thisLineTotalXAdvancement); } Tools3D.endGL(pa); } else{ //P3D rendering g.pushMatrix(); //FIXME TEST text scrolling - but IMHO better done with parent list/scroll container g.translate(totalScrollTextX, totalScrollTextY + font.getFontMaxAscent(), 0); for (int i = 0; i < charListSize; i++) { IFontCharacter character = characterList.get(i); //Step to the right by the amount of the last characters x advancement pa.translate(lastXAdvancement, 0, 0); //original //Save total amount gone to the right in this line thisLineTotalXAdvancement += lastXAdvancement; lastXAdvancement = 0; //Draw the letter character.drawComponent(g); //Check if newLine occurs, goto start at new line if (character.getUnicode().equals("\n")){ pa.translate(-thisLineTotalXAdvancement, fontHeight, 0); thisLineTotalXAdvancement = 0; lastXAdvancement = innerPaddingLeft; }else{ //If caret is showing and we are at index one before caret calc the advancement if (enableCaret && showCaret && i == charListSize - 2){ if (character.getUnicode().equals("\t")){ lastXAdvancement = character.getHorizontalDist() - character.getHorizontalDist( ) / 20; }else{ //approximated value, cant get the real one lastXAdvancement = 2 + character.getHorizontalDist() - (character.getHorizontalDist() / 3); } }else{ lastXAdvancement = character.getHorizontalDist(); } } } g.popMatrix();//FIXME TEST text scrolling - but IMHO better done with parent list/scroll container } //FIXME TEST //Innerpadding TOP for wrapped textarea -> translates the text content downwards switch (this.mode) { case MODE_EXPAND: g.popMatrix(); break; case MODE_WRAP: //Need to translate innerpadding because we shouldnt make the textarea bigger g.popMatrix(); break; default: break; } //remove caret if (enableCaret && showCaret){ characterList.remove(charListSize-1); } //FIXME TEST if (isBitmapFont && textPositionRounding && applySnap){ g.popMatrix(); } } private void drawCharactersGL(GL gl, List<IFontCharacter> characterList, int charListSize, int lastXAdv, int lineTotalAdv){ int lastXAdvancement = lastXAdv; int thisLineTotalXAdvancement = lineTotalAdv; for (int i = 0; i < charListSize; i++) { IFontCharacter character = characterList.get(i); //Step to the right by the amount of the last characters x advancement gl.glTranslatef(lastXAdvancement, 0, 0); //Save total amount gone to the right in this line thisLineTotalXAdvancement += lastXAdvancement; lastXAdvancement = 0; //Draw the letter character.drawComponent(gl); //Check if newLine occurs, goto start at new line if (character.getUnicode().equals("\n")){ gl.glTranslatef(-thisLineTotalXAdvancement, fontHeight, 0); thisLineTotalXAdvancement = 0; lastXAdvancement = innerPaddingLeft; }else{ //If caret is showing and we are at index one before caret calc the advancement to include the caret in the text area if (enableCaret && showCaret && i == charListSize-2){ if (character.getUnicode().equals("\t")){ lastXAdvancement = character.getHorizontalDist() - character.getHorizontalDist() / 20; }else{ //approximated value, cant get the real one lastXAdvancement = 2 + character.getHorizontalDist() - (character.getHorizontalDist() / 3); } }else{ lastXAdvancement = character.getHorizontalDist(); } } } } private boolean noStrokeSettingSaved; @Override public void postDraw(PGraphics graphics) { super.postDraw(graphics); //Hack for drawing anti aliased stroke outline over the clipped area if (this.mode == MODE_WRAP && this.getClip()!= null && !noStrokeSettingSaved){ this.setNoStroke(noStrokeSettingSaved); boolean noFillSavedSetting = this.isNoFill(); this.setNoFill(true); super.drawComponent(graphics);//Draw only stroke line after we ended clipping do preserve anti aliasing - hack this.setNoFill(noFillSavedSetting); } } //FIXME TEST scrolling (used in MTTextField for example) protected void scrollTextX(float amount){ this.totalScrollTextX += amount; } protected void scrollTextY(float amount){ this.totalScrollTextY += amount; } protected float getScrollTextX() { return this.totalScrollTextX; } protected float getScrollTextY() { return this.totalScrollTextY; } //FIXME TEST ? /** * Changes the texture filtering for the textarea's bitmap font. * (if a bitmap font is used). * If the parameter is "true" this will allow the text being scaled without getting * too pixelated. If the text isnt going to be scaled ever, it is best to leave or * set this to "false" for a sharper text. * <br>NOTE: Only applies if OpenGL is the renderer and the textarea uses a bitmap font. * <br>NOTE: This affects the whole bitmap font so if it is used elsewhere it is changed * there, too. * * @param scalable the new bitmap font scalable */ public void setBitmapFontTextureFiltered(boolean scalable){ if (MT4jSettings.getInstance().isOpenGlMode() && this.getFont() instanceof BitmapFont){ BitmapFont font = (BitmapFont)this.getFont(); IFontCharacter[] characters = font.getCharacters(); for (int i = 0; i < characters.length; i++) { IFontCharacter fontCharacter = characters[i]; if (fontCharacter instanceof BitmapFontCharacter) { BitmapFontCharacter bChar = (BitmapFontCharacter) fontCharacter; bChar.setTextureFiltered(scalable); } } } } /** * Sets the width local. * * @param width the new width local */ @Override public void setWidthLocal(float width){ super.setWidthLocal(width); // Vertex[] v = this.getVerticesLocal(); // MTColor c = this.getFillColor(); // this.setVertices( // new Vertex[]{ // v[0], // new Vertex(width, v[1].getY(), v[1].getZ(), c.getR(), c.getG(), c.getB(), c.getAlpha()), // new Vertex(width, v[2].getY(), v[2].getZ(), c.getR(), c.getG(), c.getB(), c.getAlpha()), // v[3], // v[4]}); switch (this.mode) { case MODE_EXPAND: break; case MODE_WRAP: //if in MODE_WRAP also reset the size of the CLIP SHAPE! if (MT4jSettings.getInstance().isOpenGlMode() && this.getClip() != null && this.getClip().getClipShape() instanceof MTRectangle){ MTRectangle clipRect = (MTRectangle)this.getClip().getClipShape(); // clipRect.setWidthLocal(this.getWidthXY(TransformSpace.LOCAL)); //Clip the text to the area // this.setClip(new Clip(pApplet, this.getVerticesLocal()[0].x, this.getVerticesLocal()[0].y, this.getWidthXY(TransformSpace.LOCAL), this.getHeightXY(TransformSpace.LOCAL))); // clipRect.setVertices(Vertex.getDeepVertexArrayCopy(this.getVerticesLocal())); clipRect.setVertices(this.getVerticesLocal()); } this.updateLayout(); break; default: break; } } /** * Sets the height local. * * @param height the new height local */ @Override public void setHeightLocal(float height){ Vertex[] v = this.getVerticesLocal(); // this.setVertices( // new Vertex[]{ // new Vertex(v[2].getX(), upperLeftLocal.y + height, v[2].getZ(), this.getFillRed(), this.getFillGreen(), this.getFillBlue(), this.getFillAlpha()), // v[0], // v[1] , // new Vertex(v[2].getX(), upperLeftLocal.y + height, v[2].getZ(), this.getFillRed(), this.getFillGreen(), this.getFillBlue(), this.getFillAlpha()), // new Vertex(v[3].getX(), upperLeftLocal.y + height, v[3].getZ(), this.getFillRed(), this.getFillGreen(), this.getFillBlue(), this.getFillAlpha()), // v[4]}); switch (this.mode) { case MODE_EXPAND: this.setVertices(new Vertex[]{ // new Vertex(v[0].x, - innerPaddingTop, v[0].z, v[0].getTexCoordU(), v[0].getTexCoordV(), v[0].getR(), v[0].getG(), v[0].getB(), v[0].getA()), // new Vertex(v[1].x, - innerPaddingTop, v[1].z, v[1].getTexCoordU(), v[1].getTexCoordV(), v[1].getR(), v[1].getG(), v[1].getB(), v[1].getA()), // new Vertex(v[2].x, - innerPaddingTop + height + (2 * innerPaddingTop), v[2].z, v[2].getTexCoordU(), v[2].getTexCoordV(), v[2].getR(), v[2].getG(), v[2].getB(), v[2].getA()), // new Vertex(v[3].x, - innerPaddingTop + height + (2 * innerPaddingTop), v[3].z, v[3].getTexCoordU(), v[3].getTexCoordV(), v[3].getR(), v[3].getG(), v[3].getB(), v[3].getA()), // new Vertex(v[4].x, - innerPaddingTop, v[4].z, v[4].getTexCoordU(), v[4].getTexCoordV(), v[4].getR(), v[4].getG(), v[4].getB(), v[4].getA()), new Vertex(v[0].x, 0, v[0].z, v[0].getTexCoordU(), v[0].getTexCoordV(), v[0].getR(), v[0].getG(), v[0].getB(), v[0].getA()), new Vertex(v[1].x, 0, v[1].z, v[1].getTexCoordU(), v[1].getTexCoordV(), v[1].getR(), v[1].getG(), v[1].getB(), v[1].getA()), new Vertex(v[2].x, height + (2 * innerPaddingTop), v[2].z, v[2].getTexCoordU(), v[2].getTexCoordV(), v[2].getR(), v[2].getG(), v[2].getB(), v[2].getA()), new Vertex(v[3].x, height + (2 * innerPaddingTop), v[3].z, v[3].getTexCoordU(), v[3].getTexCoordV(), v[3].getR(), v[3].getG(), v[3].getB(), v[3].getA()), new Vertex(v[4].x, 0, v[4].z, v[4].getTexCoordU(), v[4].getTexCoordV(), v[4].getR(), v[4].getG(), v[4].getB(), v[4].getA()), }); break; case MODE_WRAP: super.setHeightLocal(height); //if in MODE_WRAP also reset the size of the CLIP SHAPE! if (MT4jSettings.getInstance().isOpenGlMode() && this.getClip() != null && this.getClip().getClipShape() instanceof MTRectangle){ MTRectangle clipRect = (MTRectangle)this.getClip().getClipShape(); // clipRect.setVertices(Vertex.getDeepVertexArrayCopy(this.getVerticesLocal())); clipRect.setVertices(this.getVerticesLocal()); } this.updateLayout(); break; default: break; } } /** * Returns the currently active expand direction of the text area. * (Only has an impact if the wrap mode of the textarea equals MODE_EXPAND!) * * @return the active expand direction */ public ExpandDirection getExpandDirection() { return expandDirection; } /** * Sets the expand direction to be used by the text area if a new line is added. *(Only has an impact if the wrap mode of the textarea equals MODE_EXPAND!) * @see {@link ExpandDirection} * @param direction the expand direction to be used */ public void setExpandDirection(ExpandDirection direction) { expandDirection = direction; this.updateLayout(); //This wont translate the area to its original place if expand mode was UP and is now down.. } @Override public void setSizeLocal(float width, float height) { if (width > 0 && height > 0){ Vertex[] v = this.getVerticesLocal(); switch (this.mode) { case MODE_EXPAND: this.setVertices(new Vertex[]{ // new Vertex(v[0].x, - innerPaddingTop, v[0].z, v[0].getTexCoordU(), v[0].getTexCoordV(), v[0].getR(), v[0].getG(), v[0].getB(), v[0].getA()), // new Vertex(v[0].x+width, - innerPaddingTop, v[1].z, v[1].getTexCoordU(), v[1].getTexCoordV(), v[1].getR(), v[1].getG(), v[1].getB(), v[1].getA()), // new Vertex(v[0].x+width, - innerPaddingTop + height + (2 * innerPaddingTop), v[2].getTexCoordV(), v[2].getR(), v[2].getG(), v[2].getB(), v[2].getA()), // new Vertex(v[3].x, - innerPaddingTop + height + (2 * innerPaddingTop), v[3].z, v[3].getTexCoordU(), v[3].getTexCoordV(), v[3].getR(), v[3].getG(), v[3].getB(), v[3].getA()), // new Vertex(v[4].x, - innerPaddingTop, v[4].z, v[4].getTexCoordU(), v[4].getTexCoordV(), v[4].getR(), v[4].getG(), v[4].getB(), v[4].getA()), new Vertex(v[0].x, 0, v[0].z, v[0].getTexCoordU(), v[0].getTexCoordV(), v[0].getR(), v[0].getG(), v[0].getB(), v[0].getA()), new Vertex(v[0].x+width, 0, v[1].z, v[1].getTexCoordU(), v[1].getTexCoordV(), v[1].getR(), v[1].getG(), v[1].getB(), v[1].getA()), new Vertex(v[0].x+width, height + (2 * innerPaddingTop), v[2].getTexCoordV(), v[2].getR(), v[2].getG(), v[2].getB(), v[2].getA()), new Vertex(v[3].x, height + (2 * innerPaddingTop), v[3].z, v[3].getTexCoordU(), v[3].getTexCoordV(), v[3].getR(), v[3].getG(), v[3].getB(), v[3].getA()), new Vertex(v[4].x, 0, v[4].z, v[4].getTexCoordU(), v[4].getTexCoordV(), v[4].getR(), v[4].getG(), v[4].getB(), v[4].getA()), }); break; case MODE_WRAP: super.setSizeLocal(width, height); //if in MODE_WRAP also reset the size of the CLIP SHAPE! if (MT4jSettings.getInstance().isOpenGlMode() && this.getClip() != null && this.getClip().getClipShape() instanceof MTRectangle){ MTRectangle clipRect = (MTRectangle)this.getClip().getClipShape(); //clipRect.setVertices(Vertex.getDeepVertexArrayCopy(this.getVerticesLocal())); clipRect.setVertices(this.getVerticesLocal()); } this.updateLayout(); break; default: break; } } } /** * Appends the string to the textarea. * * @param string the string */ synchronized public void appendText(String string){ for (int i = 0; i < string.length(); i++) { appendCharByUnicode(string.substring(i, i+1)); } } /** * Sets the provided string as the text of this textarea. * * @param string the string */ synchronized public void setText(String string){ clear(); for (int i = 0; i < string.length(); i++) { appendCharByUnicode(string.substring(i, i+1)); } //FIXME TEST /* if (MT4jSettings.getInstance().isOpenGlMode()){ if (getRenderer() instanceof MTApplication) { MTApplication app = (MTApplication) getRenderer(); if (app.isRenderThreadCurrent()){ this.useContentDisplayList(); }else{ app.invokeLater(new Runnable() { public void run() { useContentDisplayList(); } }); } }else{ this.useContentDisplayList(); } } */ } /* (non-Javadoc) * @see org.mt4j.components.visibleComponents.widgets.keyboard.ITextInputListener#getText() */ public String getText(){ String returnString = ""; for (Iterator<IFontCharacter> iter = this.characterList.iterator(); iter.hasNext();) { IFontCharacter character = (IFontCharacter) iter.next(); String unicode = character.getUnicode(); if (!character.equals(MTTextArea.artificialLineBreak)){ returnString += unicode; } } return returnString; } /** * Append char by name. * @param characterName the character name */ synchronized public void appendCharByName(String characterName){ //Get the character from the font IFontCharacter character = font.getFontCharacterByName(characterName); if (character == null){ System.err.println("Error adding character with name '" + characterName + "' to the textarea. The font couldnt find the character. -> Trying to use 'missing glyph'"); character = font.getFontCharacterByName("missing-glyph"); if (character != null) addCharacter(character); }else{ addCharacter(character); } } /* (non-Javadoc) * @see org.mt4j.components.visibleComponents.widgets.keyboard.ITextInputListener#appendCharByUnicode(java.lang.String) */ synchronized public void appendCharByUnicode(String unicode){ //Get the character from the font IFontCharacter character = font.getFontCharacterByUnicode(unicode); if (character == null){ // System.err.println("Error adding character with unicode '" + unicode + "' to the textarea. The font couldnt find the character. ->Trying to use 'missing glyph'"); character = font.getFontCharacterByUnicode("missing-glyph"); if (character != null) addCharacter(character); }else{ addCharacter(character); } } /** * Gets the characters. Also returns articifially added new line characters that were * added by the MTTextArea * @return the characters */ public IFontCharacter[] getCharacters(){ return this.characterList.toArray(new IFontCharacter[this.characterList.size()]); } /** * Adds the character. * * @param character the character */ private void addCharacter(IFontCharacter character){ this.characterList.add(character); this.characterAdded(character); this.setContentDisplayListDirty(true); } /** * Invoked everytime a character is added. * * @param character the character */ protected void characterAdded(IFontCharacter character){ switch (this.mode) { case MODE_EXPAND: if (character.getUnicode().equals("\n")){ //Expand vertically at enter this.setHeightLocal(this.getTotalLinesHeight()); //Moves the Textarea up at a enter character instead of down if (getExpandDirection() == ExpandDirection.UP) this.translate(new Vector3D(0, -fontHeight, 0)); }else{ //Expand the textbox to the extend of the widest line width this.setWidthLocal(getMaxLineWidth()); } break; case MODE_WRAP: float localWidth = this.getWidthXY(TransformSpace.LOCAL); // float maxLineWidth = this.getMaxLineWidth(); float maxLineWidth = this.getLastLineWidth(); if (this.characterList.size() > 0 && maxLineWidth > localWidth ) { // if (this.characterList.size() > 0 && maxLineWidth > (localWidth - 2 * this.getInnerPaddingLeft())) { // this.characterList.add(this.characterList.size() -1 , this.font.getFontCharacterByUnicode("\n")); try { int lastSpacePos = getLastWhiteSpace(); if (lastSpacePos != -1 ){ //&& !this.characterList.get(characterList.size()-1).getUnicode().equals("\n") // this.characterList.add(lastSpacePos + 1, this.font.getFontCharacterByUnicode("\n")); this.characterList.add(lastSpacePos + 1, MTTextArea.artificialLineBreak); }else{ return; } } catch (Exception e) { e.printStackTrace(); } } break; default: break; } } private int getLastWhiteSpace(){ for (int i = this.characterList.size()-1; i > 0; i--) { IFontCharacter character = this.characterList.get(i); if (character.getUnicode().equals(" ")){ return i; }else if (character.getUnicode().equals("\n")){// stop search when newline found before first whitespace return -1; } } return -1; } /** * When Character removed. * * @param character the character */ protected void characterRemoved(IFontCharacter character){ switch (this.mode) { case MODE_EXPAND: //Resize text field if (character.getUnicode().equals("\n")){ //Reduce field vertically at enter this.setHeightLocal(this.getTotalLinesHeight()); //makes the textarea go down when a line is removed instead staying at the same loc. if (getExpandDirection() == ExpandDirection.UP) translate(new Vector3D(0, fontHeight, 0)); }else{ //Reduce field horizontally this.setWidthLocal(getMaxLineWidth()); } break; case MODE_WRAP: break; default: break; } } /** * resets the textarea, clears all characters. */ public void clear(){ while (!characterList.isEmpty()){ removeLastCharacter(); } } /** * Removes the last character in the textarea. */ synchronized public void removeLastCharacter(){ if (this.characterList.isEmpty()) return; //REMOVE THE CHARACTER IFontCharacter lastCharacter = this.characterList.get(this.characterList.size()-1); this.characterList.remove(this.characterList.size()-1); this.characterRemoved(lastCharacter); this.setContentDisplayListDirty(true); } /** * Gets the last line width. * * @return the last line width */ protected float getLastLineWidth(){ float currentLineWidth = 2 * this.getInnerPaddingLeft() + caretWidth; for (int i = 0; i < this.characterList.size(); i++) { IFontCharacter character = this.characterList.get(i); if (character.getUnicode().equals("\n")){ currentLineWidth = 2 * this.getInnerPaddingLeft() + caretWidth;; }else{ currentLineWidth += character.getHorizontalDist(); } } return currentLineWidth; } /** * Gets the max line width. The padding is also added. * * @return the max line width */ protected float getMaxLineWidth(){ float currentLineWidth = 2 * this.getInnerPaddingLeft() + caretWidth; float maxWidth = currentLineWidth; for (int i = 0; i < this.characterList.size(); i++) { IFontCharacter character = this.characterList.get(i); if (character.getUnicode().equals("\n")){ if (currentLineWidth > maxWidth){ maxWidth = currentLineWidth; } currentLineWidth = 2 * this.getInnerPaddingLeft() + caretWidth; }else{ currentLineWidth += character.getHorizontalDist(); if (currentLineWidth > maxWidth){ maxWidth = currentLineWidth; } } } return maxWidth; } /** * Gets the total lines height. Padding is not included * * @return the total lines height */ protected float getTotalLinesHeight(){ float height = fontHeight ;// for (int i = 0; i < this.characterList.size(); i++) { IFontCharacter character = this.characterList.get(i); if (character.getUnicode().equals("\n")){ height += fontHeight; } } return height; } public void setInnerPadding(int innerPadding){ this.setInnerPaddingTop(innerPadding); this.setInnerPaddingLeft(innerPadding); } public float getInnerPaddingTop() { return this.innerPaddingTop; } public void setInnerPaddingTop(int innerPaddingTop) { this.innerPaddingTop = innerPaddingTop; switch (this.mode) { case MODE_EXPAND: //At MODE_EXPAND we re-set the text so the size gets re-calculated //We can safely do this since in EXPAND mode we didnt add any artificial control characters this.updateLayout(); break; case MODE_WRAP: //At MODE_WRAP the padding is done with gl_Translate calls so we dont have to reset the size //TODO also reset? this.setText(this.getText());? break; default: break; } } public float getInnerPaddingLeft() { return this.innerPaddingLeft; } public void setInnerPaddingLeft(int innerPaddingLeft) { this.innerPaddingLeft = innerPaddingLeft; switch (this.mode) { case MODE_EXPAND: //At MODE_EXPAND we re-set the text so the size gets re-calculated //We can safely do this since in EXPAND mode we didnt add any artificial control characters this.updateLayout(); break; case MODE_WRAP: // WE HAVE TO RESET THE ORIGINAL TEXT BECAUSE WE BREAK THE LINE AT DIFFERENT POSITIONS IF THE INNERPADDING IS CHANGED! this.updateLayout(); break; default: break; } } /** * Updates layout. (just does this.setText(this.getText())) */ protected void updateLayout(){ if (this.mode == MODE_EXPAND){ this.setHeightLocal(this.getTotalLinesHeight()); this.setWidthLocal(getMaxLineWidth()); } this.setText(this.getText()); } /** * Gets the line count. * * @return the line count */ public int getLineCount(){ int count = 0; for (int i = 0; i < this.characterList.size(); i++) { IFontCharacter character = this.characterList.get(i); if (character.getUnicode().equals("\n")){ count++; } } return count; } /** * Gets the font. * * @return the font */ public IFont getFont() { return font; } /** * Snap to keyboard. * * @param mtKeyboard the mt keyboard */ public void snapToKeyboard(MTKeyboard mtKeyboard){ //OLD WAY // this.translate(new Vector3D(30, -(getFont().getFontAbsoluteHeight() * (getLineCount())) + getFont().getFontMaxDescent() - borderHeight, 0)); mtKeyboard.addChild(this); this.setPositionRelativeToParent(new Vector3D(40, -this.getHeightXY(TransformSpace.LOCAL)*0.5f)); } public boolean isSelected() { // TODO Auto-generated method stub return false; } public void setSelected(boolean selected) { // TODO Auto-generated method stub } /** * Checks if is enable caret. * * @return true, if is enable caret */ public boolean isEnableCaret() { return enableCaret; } /** * Sets the enable caret. * * @param enableCaret the new enable caret */ public void setEnableCaret(boolean enableCaret) { if (this.getFont().getFontCharacterByUnicode("|") != null){ this.enableCaret = enableCaret; if (enableCaret){ this.caretWidth = 10; }else{ this.caretWidth = 0; } if (this.mode == MODE_EXPAND){ this.setWidthLocal(this.getMaxLineWidth()); } }else{ System.err.println("Cant enable caret for this textfield, the font doesent include the letter '|'"); } this.setContentDisplayListDirty(true); } public int compareTo(Object o) { if (o instanceof MTTextArea) { MTTextArea ta = (MTTextArea)o; return this.getText().compareToIgnoreCase(ta.getText()); } else { return 0; } } /** * Artifical line break to be used instead of the regular line break * to indicate that this linebreak was added by the text area itself for * layout reasons and doesent really belong to the supplied text. * * @author Christopher Ruff */ private class ArtificalLineBreak implements IFontCharacter{ public void drawComponent(PGraphics g) {} public void drawComponent(GL gl) { } public void destroy() { } public int getHorizontalDist() { return 0; } public String getUnicode() { return "\n"; } } }