/* * Copyright (c) 2003-onwards Shaven Puppy Ltd * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'Shaven Puppy' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.shavenpuppy.jglib.opengl; import java.io.ObjectStreamException; import java.io.Serializable; import org.lwjgl.Sys; import org.lwjgl.input.Keyboard; import org.lwjgl.util.Dimension; import org.lwjgl.util.Point; import org.lwjgl.util.ReadableColor; import org.lwjgl.util.ReadableDimension; import org.lwjgl.util.ReadablePoint; import org.lwjgl.util.ReadableRectangle; import org.lwjgl.util.Rectangle; import org.lwjgl.util.WritableDimension; import org.lwjgl.util.WritablePoint; import com.shavenpuppy.jglib.Glyph; import com.shavenpuppy.jglib.TextLayout; import com.shavenpuppy.jglib.sprites.SimpleRenderable; import com.shavenpuppy.jglib.sprites.SimpleRenderer; import com.shavenpuppy.jglib.util.Decodeable; import static org.lwjgl.opengl.GL11.*; /** * Displays text in a box, automatically wrapping words. * @author: cas */ public class GLTextArea implements SimpleRenderable, WritablePoint, WritableDimension { private static final long serialVersionUID = 1L; // The font to use protected GLFont font; // The text to display private final StringBuilder text = new StringBuilder(); // The laid-out glyphs (not all elements will be used) private GLGlyph[] glyph; // The number of glyphs used in the glyph[] array private int numGlyphs; // The size of the text area private final Dimension size = new Dimension(); // The calculated text height private int textHeight; // The location private final Point location = new Point(); // The formatter private TextLayout.Format format = TextLayout.WORD_WRAP; // The text alignment, from TextLayout private TextLayout.HorizontalAlignment alignment = TextLayout.LEFT; // Vertical alignment private VerticalAlignment verticalAlignment = TOP; /** Changed flag */ private boolean changed = true; /** Whether to colour text */ private boolean coloured; private ReadableColor topColour, bottomColour; private int alpha = 255; /** Leading */ private int leading; /** Vertical Alignments */ public abstract static class VerticalAlignment implements Serializable, Decodeable{ private static final long serialVersionUID = 1L; private final String display; private VerticalAlignment(String display) { this.display = display; } @Override public String toString() { return display; } public static Object decode(String in) throws Exception { if (in.equalsIgnoreCase(TOP.display)) { return TOP; } else if (in.equalsIgnoreCase(BOTTOM.display)) { return BOTTOM; } else if (in.equalsIgnoreCase(BASELINE.display)) { return BASELINE; } else if (in.equalsIgnoreCase(CENTERED.display)) { return CENTERED; } else { throw new Exception("Unknown vertical alignment '"+in+"'"); } } } /** * The text is drawn flush with the top of the text area. */ public static final VerticalAlignment TOP = new VerticalAlignment("Top") { private static final long serialVersionUID = 1L; private Object readResolve() throws ObjectStreamException { return TOP; } }; /** * The text is drawn flush with the bottom of the text area. */ public static final VerticalAlignment BOTTOM = new VerticalAlignment("Bottom") { private static final long serialVersionUID = 1L; private Object readResolve() throws ObjectStreamException { return BOTTOM; } }; /** * Vertically centres the text in the textarea's bounds */ public static final VerticalAlignment CENTERED = new VerticalAlignment("Centered") { private static final long serialVersionUID = 1L; private Object readResolve() throws ObjectStreamException { return CENTERED; } }; /** * Baseline alignment. The first line of glyphs is placed on <strong>top</strong> of the * text area's bounding box; subsequent lines are underneath. */ public static final VerticalAlignment BASELINE = new VerticalAlignment("Baseline") { private static final long serialVersionUID = 1L; private Object readResolve() throws ObjectStreamException { return BASELINE; } }; /* * Editing stuff */ /** Old text */ private String oldText; /** Are we editing? */ private boolean editing; /** Is the cursor currently visible? */ private boolean cursorVisible; /** Current cursor position in the string */ private int cursorPos; /** Cursor flash tick */ private int flashTick; /** * GLTextArea constructor comment. */ public GLTextArea() { } /** * Accessor * @return com.powersolve.opengl.GLFont */ public GLFont getFont() { return font; } /** * @return Returns the final coordinates of the specified glyph */ public Rectangle getGlyphBounds(int index, Rectangle dest) { layout(); if (dest == null) { dest = new Rectangle(); } if (index < 0) { dest.setBounds(location.getX(), location.getY(), 0, font.getAscent() + font.getDescent()); } else { GLGlyph g = glyph[index]; if (g != null) { dest.setBounds(g.xpos + location.getX(), g.ypos + location.getY(), g.getAdvance(), font.getAscent() + font.getDescent()); } } return dest; } /** * Returns the glyph at the specified location in the string */ public GLGlyph getGlyph(int index) { return glyph[index]; } /** * @param leading the leading to set */ public void setLeading(int leading) { this.leading = leading; changed = true; } /** * @return the leading */ public int getLeading() { return leading; } /** * Performs a layout of the text. This should be performed if the text or font is changed or the size of * the text area is adjusted. */ private void layout() { if (!changed) { return; } changed = false; // Create initial layout assert text != null : this+" has no text"; assert size != null : this+" has no size"; assert font != null : this+" has no font"; TextLayout textLayout = new TextLayout(font.getFont(), font.getScale(), leading, text.toString(), size.getWidth(), format, alignment); if (editing) { textLayout.setKeepWhiteSpace(true); } // Now we know how big the text was: textHeight = textLayout.getHeight(); // Now align to given box. Currently the glyphs baseline is at 0,0 and they stretch downwards into negative // coordinates. If TOP aligned then need shifting up by -penY. If BOTTOM aligned then they need shifting up // by the specified height minus penY. final int ty; if (verticalAlignment == TOP) { // Translate all glyphs up ty = size.getHeight(); } else if (verticalAlignment == CENTERED) { // Translate all glyphs up ty = textHeight + (size.getHeight() - textHeight) / 2; } else if (verticalAlignment == BASELINE) { // Move by first ascent ty = font.getAscent(); } else { // Translate all glyphs up ty = textHeight; } // Get all the glyphs textLayout.apply(new TextLayout.Target() { /** * Tells the target how many glyphs will be set. Called first. */ @Override public void setNumGlyphs(int n) { numGlyphs = n; if (glyph == null || glyph.length < numGlyphs || glyph.length > numGlyphs * 2) { glyph = null; glyph = new GLGlyph[numGlyphs]; } } /** * Each glyph is set via this method */ @Override public void setGlyph(int index, Glyph g, int x, int y) { if (glyph[index] == null) { glyph[index] = new GLGlyph(font.getTexture(), g, font.getScale()); } else { glyph[index].init(font.getTexture(), g); } glyph[index].setLocation(x + location.getX(), y + ty + location.getY()); } }); } @Override public void render(SimpleRenderer renderer) { layout(); renderer.glRender(new GLRenderable() { @Override public void render() { // Bind the font's texture first font.getTexture().render(); glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); } }); //renderer.glBegin(GL_QUADS); renderGlyphs(renderer); //renderer.glEnd(); // Maybe draw cursor? if (cursorVisible && editing) { renderer.glRender(new GLRenderable() { @Override public void render() { glDisable(GL_TEXTURE_2D); } }); int cursorX; int cursorY; if (cursorPos <= 0 || glyph.length == 0) { cursorPos = 0; cursorX = 0; cursorY = getHeight() - font.getAscent(); } else { if (cursorPos > glyph.length) { cursorPos = glyph.length; } cursorX = glyph[cursorPos - 1].getXpos() + glyph[cursorPos - 1].getWidth(); cursorY = glyph[cursorPos - 1].getYpos(); } renderer.glColor(bottomColour); short idx = renderer.glVertex2f(cursorX, cursorY - font.getDescent()); renderer.glVertex2f(cursorX + 4, cursorY - font.getDescent()); renderer.glColor(topColour); renderer.glVertex2f(cursorX + 4, cursorY + font.getHeight() - font.getDescent()); renderer.glVertex2f(cursorX, cursorY + font.getHeight() - font.getDescent()); renderer.glRender(GL_TRIANGLE_FAN, new short[] {(short) (idx + 0), (short) (idx + 1), (short) (idx + 2), (short) (idx + 3)}); } } protected void renderGlyphs(SimpleRenderer renderer) { for (int i = 0; i < numGlyphs; i++) { if (glyph[i] != null) { if (coloured) { glyph[i].render(topColour, bottomColour, alpha, renderer); } else { glyph[i].render(renderer); } } } } /** * @return the numGlyphs */ public int getNumGlyphs() { return numGlyphs; } /** * Sets the font */ public void setFont(GLFont font) { if (this.font == font) { return; } this.font = font; setLeading(font.getLeading()); changed = true; } /** * Set the horizontal alignment */ public void setHorizontalAlignment(TextLayout.HorizontalAlignment alignment) { if (this.alignment == alignment) { return; } this.alignment = alignment; changed = true; } /** * Set the vertical alignement */ public void setVerticalAlignment(VerticalAlignment valignment) { if (this.verticalAlignment == valignment) { return; } this.verticalAlignment = valignment; changed = true; } /** * Returns the calculated text height. You can use this to resize the * text area if you like. */ public int getTextHeight() { layout(); return textHeight; } /** * Gets the wrap. * @return TextLayout.Format */ public TextLayout.Format getFormat() { return format; } /** * Sets the text wrapping. * @param wrap The wrap to set */ public void setFormat(TextLayout.Format format) { if (this.format == format) { return; } this.format = format; changed = true; } /* * StringBuilder pseudo-delegate methods. Instead of returning the StringBuilder * which would allow people to dick around with the text inappropriately, we * return the GLTextArea instead. */ /** * @param b * @return */ public GLTextArea append(boolean b) { changed = true; text.append(b); return this; } /** * @param c * @return */ public GLTextArea append(char c) { changed = true; text.append(c); return this; } /** * @param str * @return */ public GLTextArea append(char[] str) { changed = true; text.append(str); return this; } /** * @param str * @param offset * @param len * @return */ public GLTextArea append(char[] str, int offset, int len) { changed = true; text.append(str, offset, len); return this; } /** * @param d * @return */ public GLTextArea append(double d) { changed = true; text.append(d); return this; } /** * @param f * @return */ public GLTextArea append(float f) { changed = true; text.append(f); return this; } /** * @param i * @return */ public GLTextArea append(int i) { changed = true; text.append(i); return this; } /** * @param obj * @return */ public GLTextArea append(Object obj) { changed = true; text.append(obj); return this; } /** * @param str * @return */ public GLTextArea append(String str) { changed = true; text.append(str); return this; } /** * @param sb * @return */ public GLTextArea append(StringBuilder sb) { changed = true; text.append(sb); return this; } /** * @param l * @return */ public GLTextArea append(long l) { changed = true; text.append(l); return this; } /** * @return */ public int capacity() { return text.capacity(); } /** * @param start * @param end * @return */ public GLTextArea delete(int start, int end) { changed = true; text.delete(start, end); return this; } /** * @param index * @return */ public GLTextArea deleteCharAt(int index) { changed = true; text.deleteCharAt(index); return this; } /** * @param offset * @param b * @return */ public GLTextArea insert(int offset, boolean b) { changed = true; text.insert(offset, b); return this; } /** * @param offset * @param c * @return */ public GLTextArea insert(int offset, char c) { changed = true; text.insert(offset, c); return this; } /** * @param offset * @param str * @return */ public GLTextArea insert(int offset, char[] str) { changed = true; text.insert(offset, str); return this; } /** * @param index * @param str * @param offset * @param len * @return */ public GLTextArea insert(int index, char[] str, int offset, int len) { changed = true; text.insert(index, str, offset, len); return this; } /** * @param offset * @param d * @return */ public GLTextArea insert(int offset, double d) { changed = true; text.insert(offset, d); return this; } /** * @param offset * @param f * @return */ public GLTextArea insert(int offset, float f) { changed = true; text.insert(offset, f); return this; } /** * @param offset * @param i * @return */ public GLTextArea insert(int offset, int i) { changed = true; text.insert(offset, i); return this; } /** * @param offset * @param obj * @return */ public GLTextArea insert(int offset, Object obj) { changed = true; text.insert(offset, obj); return this; } /** * @param offset * @param str * @return */ public GLTextArea insert(int offset, String str) { changed = true; text.insert(offset, str); return this; } /** * @param offset * @param l * @return */ public GLTextArea insert(int offset, long l) { changed = true; text.insert(offset, l); return this; } /** * @return */ public int length() { return text.length(); } /** * @param start * @param end * @param str * @return */ public GLTextArea replace(int start, int end, String str) { changed = true; text.replace(start, end, str); return this; } /** * @param index * @param ch */ public void setCharAt(int index, char ch) { if (text.charAt(index) == ch) { return; } changed = true; text.setCharAt(index, ch); } /** * @param newLength */ public void setLength(int newLength) { if (newLength == text.length()) { return; } changed = true; text.setLength(newLength); } /* * WritableRectangle delegate methods */ /** * @param dest */ public void getLocation(WritablePoint dest) { location.getLocation(dest); } /** * @return */ public int getX() { return location.getX(); } /** * @return */ public int getY() { return location.getY(); } /** * @param x * @param y */ @Override public void setLocation(int x, int y) { if (location.getX() == x && location.getY() == y) { return; } changed = true; location.setLocation(x, y); } /** * @param p */ @Override public void setLocation(ReadablePoint p) { if (location.getX() == p.getX() && location.getY() == p.getY()) { return; } changed = true; location.setLocation(p); } /** * @param x */ @Override public void setX(int x) { if (location.getX() == x) { return; } changed = true; location.setX(x); } /** * @param y */ @Override public void setY(int y) { if (location.getY() == y) { return; } changed = true; location.setY(y); } /** * @param dx * @param dy */ public void translate(int dx, int dy) { if (dx == 0 && dy == 0) { return; } changed = true; location.translate(dx, dy); } /** * @param p */ public void translate(ReadablePoint p) { if (p.getX() == 0 && p.getY() == 0) { return; } changed = true; location.translate(p); } /** * @param p */ public void untranslate(ReadablePoint p) { if (p.getX() == 0 && p.getY() == 0) { return; } changed = true; location.untranslate(p); } /** * @return */ public int getHeight() { layout(); return size.getHeight(); } /** * @return */ public int getWidth() { layout(); return size.getWidth(); } /** * @param height */ @Override public void setHeight(int height) { if (size.getHeight() == height) { return; } changed = true; size.setHeight(height); } /** * @param width */ @Override public void setWidth(int width) { if (size.getWidth() == width) { return; } changed = true; size.setWidth(width); } /** * @param dest */ public void getSize(WritableDimension dest) { layout(); size.getSize(dest); } public void setBounds(int x, int y, int w, int h) { setLocation(x, y); setSize(w, h); } public void setBounds(ReadablePoint p, ReadableDimension d) { setLocation(p); setSize(d); } public void setBounds(ReadableRectangle r) { setLocation(r); setSize(r); } /** * @param w * @param h */ @Override public void setSize(int w, int h) { if (size.getWidth() == w && size.getHeight() == h) { return; } changed = true; size.setSize(w, h); } /** * @param d */ @Override public void setSize(ReadableDimension d) { if (size.equals(d)) { return; } changed = true; size.setSize(d); } /** * @return verticalAlignment. */ public VerticalAlignment getVerticalAlignment() { return verticalAlignment; } /** * @return alignment. */ public TextLayout.HorizontalAlignment getHorizontalAlignment() { return alignment; } /** * Set the text * @param newText, may not be null */ public void setText(String newText) { text.setLength(0); text.append(newText); changed = true; cursorPos = newText.length(); } /** * @return the text */ public String getText() { return text.toString(); } /** * @param coloured the coloured to set */ public void setColoured(boolean coloured) { this.coloured = coloured; } /** * @return the coloured */ public boolean isColoured() { return coloured; } public void setColour(ReadableColor c) { setColoured(true); bottomColour = c; topColour = c; } public void setBottomColour(ReadableColor c) { setColoured(true); bottomColour = c; } public void setTopColour(ReadableColor c) { setColoured(true); topColour = c; } public void tick() { if (editing) { // We're editing flashCursor(); processKeyboard(); } } private void flashCursor() { flashTick ++; if (flashTick > 6) { flashTick = 0; cursorVisible = !cursorVisible; } } private void processKeyboard() { int oldCursorPos = cursorPos; while (Keyboard.next()) { if (!Keyboard.getEventKeyState()) { continue; } int key = Keyboard.getEventKey(); switch (key) { case Keyboard.KEY_DOWN: case Keyboard.KEY_END: cursorPos = text.length(); break; case Keyboard.KEY_UP: case Keyboard.KEY_HOME: cursorPos = 0; break; case Keyboard.KEY_LEFT: if (Keyboard.isKeyDown(Keyboard.KEY_LSHIFT)) { cursorPos = 0; } else if (cursorPos > 0) { cursorPos --; } break; case Keyboard.KEY_RIGHT: if (Keyboard.isKeyDown(Keyboard.KEY_LSHIFT)) { cursorPos = text.length(); } else if (cursorPos < text.length()) { cursorPos ++; } break; case Keyboard.KEY_DELETE: if (Keyboard.isKeyDown(Keyboard.KEY_LSHIFT)) { text.setLength(0); cursorPos = 0; changed = true; } else if (cursorPos < text.length()) { text.deleteCharAt(cursorPos); changed = true; } onEdited(); break; case Keyboard.KEY_BACK: if (Keyboard.isKeyDown(Keyboard.KEY_LSHIFT)) { text.setLength(0); cursorPos = 0; changed = true; } else { if (cursorPos > 0) { text.deleteCharAt(-- cursorPos); changed = true; } } onEdited(); break; case Keyboard.KEY_TAB: case Keyboard.KEY_RETURN: // Change focus changeFocus(); return; case Keyboard.KEY_ESCAPE: // Cancel edits cancel(); return; default: // Type this character char c = Keyboard.getEventCharacter(); if (c == 22 || key == Keyboard.KEY_INSERT && Keyboard.isKeyDown(Keyboard.KEY_LSHIFT)) { String paste = Sys.getClipboard(); if (paste == null) { break; } for (int i = 0; i < paste.length(); i ++) { c = Character.toLowerCase(paste.charAt(i)); if (text.length() < text.capacity() && acceptChar(c)) { text.insert(cursorPos ++, c); changed = true; onEdited(); } else { break; } } } else if (c == 26 && Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) { undo(); } else if (c >= 32 && c < 127) { if (acceptChar(c)) { text.insert(cursorPos ++, c); changed = true; onEdited(); } } break; } } if (oldCursorPos != cursorPos) { cursorVisible = true; flashTick = 0; } } public boolean acceptChar(char c) { return true; } /** * Change focus to something else. Called on TAB or RETURN */ public final void changeFocus() { editing = false; cursorVisible = false; flashTick = 0; onChangeFocus(); } protected void onChangeFocus() { } protected void onEdited() { } public boolean isEditing() { return editing; } /** * Sets this text field to "editing" mode or not * @param editing */ public void setEditing(boolean editing) { this.editing = editing; if (editing) { oldText = text.toString(); changed = true; } } /** * Undo all edits. Does nothing if not currently editing. */ public void undo() { if (editing) { setText(oldText); } } /** * Cancel editing programmatically. Also called when user taps ESC. */ public void cancel() { undo(); setEditing(false); onCancelled(); } /** * Called when editing is cancelled */ protected void onCancelled() { } public void setAlpha(int alpha) { this.alpha = alpha; } }