src.graphics.widgets.Text.java Source code

Java tutorial

Introduction

Here is the source code for src.graphics.widgets.Text.java

Source

/**  
  *  Written by Morgan Allen.
  *  I intend to slap on some kind of open-source license here in a while, but
  *  for now, feel free to poke around for non-commercial purposes.
  */

package src.graphics.widgets;

import src.graphics.widgets.Alphabet.Letter;
import src.user.Description; //  TODO:  Remove out-of-package references?
import src.util.*;
import src.graphics.common.*;
import org.lwjgl.opengl.*;
import java.awt.event.KeyEvent;

/**  A text object that wraps will continue onto subsequent lines when a given
  *  line is filled.  Non-wrapping text will adjust width to match current
  *  entries.
  */
public class Text extends UINode implements Description {

    final public static Alphabet INFO_FONT = new Alphabet("media/GUI/", "FontVerdana.gif", "FontVerdana.gif",
            "FontVerdana.map", 8, 16);
    final public static Colour LINK_COLOUR = new Colour().set(0.2f, 0.6f, 0.8f, 1);

    public float scale = 1.0f;

    final protected Alphabet alphabet;
    private boolean needsFormat = false;
    private Scrollbar scrollbar;

    protected List<Box2D> allEntries = new List<Box2D>();
    private Box2D fullSize = new Box2D();
    private float oldWide, oldHigh = 0;

    /**  Assorted constructors.  If 'grow' is true, the text will expand to fit
      *  the total size of the String given.  If 'wrap' is true, width will
      *  remain fixed but text will wrap around from side to side.  If 'roll'
      *  is true, then text beyond size limits will be hidden, but the last
      *  entries will remain visible.
      */
    public Text(HUD myHUD, Alphabet a) {
        this(myHUD, a, "");
    }

    public Text(HUD myHUD, Alphabet a, String s) {
        super(myHUD);
        alphabet = a;
        setText(s);
    }

    //
    //  TODO:  Scrollbars should be possible to associate with arbitrary UI
    //  groups.  In fact, they should probably be a group type unto themselves.
    public Scrollbar getScrollBar() {
        final Scrollbar bar = new Scrollbar(myHUD, Scrollbar.SCROLL_TEX, fullSize, true);
        final float W = Scrollbar.DEFAULT_SCROLL_WIDTH;
        bar.relBound.set(relBound.xmax(), relBound.ypos(), 0, relBound.ydim());
        bar.absBound.set(absBound.xmax(), absBound.ypos(), W, absBound.ydim());
        return this.scrollbar = bar;
    }

    /**  Essential component classes and interfaces- clickables can be used to
      *  navigate to other objects, with image and text entries provide
      *  information or emphasis-
      */
    public static interface Clickable {
        String fullName();

        void whenClicked();
    }

    static class ImageEntry extends Box2D {
        Image graphic;
        boolean visible;
        int wide, high;
    }

    static class TextEntry extends Box2D {
        char key;
        Letter letter;
        boolean visible;
        Colour colour = null;
        Clickable link = null;
    }

    /**  Various overrides of UINode functionality-
      */
    protected void updateAbsoluteBounds() {
        super.updateAbsoluteBounds();
        if ((oldWide != xdim()) || (oldHigh != ydim())) {
            needsFormat = true;
            oldWide = xdim();
            oldHigh = ydim();
        }
        if (needsFormat && (allEntries.size() > 0))
            format(xdim());
        //needsFormat = false ;
    }

    /**  Adds the given String to this text object in association with
      *  the specified selectable.
      */
    public void append(String s, Clickable link, Colour c) {
        if (s == null)
            s = "(none)";
        for (int n = 0, l = s.length(); n < l; n++)
            addEntry(s.charAt(n), link, c);
    }

    public void append(Clickable l, Colour c) {
        if (l == null)
            append("(none)");
        else
            append(l.fullName(), l, c);
    }

    public void append(Clickable l) {
        if (l == null)
            append("(none)");
        else
            append(l.fullName(), l, LINK_COLOUR);
    }

    public void append(Object o) {
        if (o instanceof Clickable)
            append((Clickable) o);
        else if (o != null)
            append(o.toString());
        else
            append("(none)");
    }

    public void append(String s, Clickable l) {
        append(s, l, LINK_COLOUR);
    }

    public void append(String s, Colour c) {
        append(s, null, c);
    }

    public void append(String s) {
        append(s, null, null);
    }

    public void appendList(String s, Object... l) {
        if (l.length == 0)
            return;
        append(s);
        int i = 0;
        for (Object o : l) {
            append(o);
            if (++i < l.length)
                append(", ");
        }
    }

    public void appendList(String s, Series l) {
        appendList(s, l.toArray());
    }

    /**  Adds a single image entry to this text object.  Images are used as
      *  'bullets' to indent and separate text, and this needsFormat is retained until
      *  the next carriage return or another image is inserted.
      */
    public boolean insert(Texture texGraphic, int maxSize) {
        return insert(new Image(myHUD, texGraphic), maxSize);
    }

    public boolean insert(Image graphic, int maxSize) {
        if (graphic == null)
            return false;
        graphic.absBound.set(0, 0, maxSize, maxSize);
        graphic.relBound.set(0, 0, 0, 0);
        graphic.updateRelativeParent();
        graphic.updateAbsoluteBounds();
        final ImageEntry entry = new ImageEntry();
        entry.graphic = graphic;
        entry.wide = (int) graphic.xdim();
        entry.high = (int) graphic.ydim();
        allEntries.add(entry);
        needsFormat = true;
        return true;
    }

    public void cancelBullet() {
        //  TODO:  Get rid of the indent effect associated with the last image?
    }

    /**  Adds a single letter entry to this text object.
      */
    boolean addEntry(char k, Clickable links, Colour c) {
        Letter l = null;
        if (((l = alphabet.map[k]) == null) && (k != '\n'))
            return false;
        final TextEntry entry = new TextEntry();
        entry.key = k;
        entry.letter = l;
        entry.colour = c;
        entry.link = links;
        allEntries.addLast(entry);
        needsFormat = true;
        return true;
    }

    /**  Sets this text object to the given string.
      */
    public void setText(String s) {
        allEntries.clear();
        append(s, null, null);
        needsFormat = true;
    }

    /**  Gets the string this text object contains.
      */
    public String getText() {
        int n = 0;
        char charS[] = new char[allEntries.size()];
        for (Box2D entry : allEntries) {
            if (entry instanceof TextEntry)
                charS[n++] = ((TextEntry) entry).key;
            else
                charS[n++] = '*';
        }
        return new String(charS);
    }

    /**  Handles any key press this text is registered to listen for.
      */
    void keyPress(char k) {
        switch (k) {
        case (KeyEvent.VK_BACK_SPACE):
        case (KeyEvent.VK_DELETE):
            if (allEntries.size() > 0) {
                allEntries.removeLast(); //delete last letter.
                needsFormat = true;
            }
            break;
        case (KeyEvent.VK_ESCAPE):
        case (KeyEvent.VK_ENTER):
        case (KeyEvent.VK_ACCEPT):
            escaped();
            break;
        default:
            addEntry(k, null, null);
        }
    }

    /**  Called when edit mode is escaped.
      */
    void escaped() {
        I.say("exiting edit mode...");
    }

    /**  Returns the selectable associated with the currently hovered unit of
      *  text.
      */
    protected Clickable getTextLink(Vec2D mousePos, Box2D textArea) {
        if (myHUD.selected() != this)
            return null;
        final float mX = mousePos.x + textArea.xpos() - xpos(), mY = mousePos.y + textArea.ypos() - ypos();
        if (!textArea.contains(mX, mY))
            return null;
        Box2D box = new Box2D();
        for (Box2D entry : allEntries) {
            box.set(entry.xpos() - 1, entry.ypos() - 1, entry.xdim() + 2, entry.ydim() + 2);
            if (box.contains(mX, mY) && entry.containedBy(textArea)) {
                if (entry instanceof TextEntry)
                    return ((TextEntry) entry).link;
                return null;
            }
        }
        return null;
    }

    /**  Draws this text object.
      */
    protected void render() {
        if (allEntries.size() == 0)
            return;
        //
        //  First, determine a 'viewing window' for the text, if larger than the
        //  visible pane-
        final Box2D textArea = new Box2D().set(0, 0, xdim(), ydim());
        if (scrollbar != null) {
            final float lineHigh = 0;// alphabet.map[' '].height * scale ;
            textArea.ypos((0 - lineHigh) - (fullSize.ydim() - ydim()) * (1 - scrollbar.scrollPos()));
        }
        //
        //  See what was clicked, if anything-
        //  TODO:  Move this to the selection method?
        final Clickable link = getTextLink(myHUD.mousePos(), textArea);
        final Batch<ImageEntry> bullets = new Batch<ImageEntry>();
        //
        //  Begin the rendering pass...
        alphabet.fontTex.bindTex();
        GL11.glEnable(GL11.GL_SCISSOR_TEST);
        GL11.glScissor((int) xpos(), (int) ypos(), (int) xdim(), (int) ydim());
        GL11.glBegin(GL11.GL_QUADS);
        for (Box2D entry : allEntries) {
            if (entry instanceof TextEntry) {
                renderText(textArea, (TextEntry) entry, link);
            } else
                bullets.add((ImageEntry) entry);
        }
        GL11.glEnd();
        for (ImageEntry entry : bullets) {
            renderImage(textArea, entry);
        }
        GL11.glDisable(GL11.GL_SCISSOR_TEST);
        //  TODO:  Move this to the selection method?
        if (myHUD.mouseClicked() && link != null)
            link.whenClicked();
    }

    /**  Renders an image embedded within the text.
      */
    protected void renderImage(Box2D bounds, ImageEntry entry) {
        if (!entry.intersects(bounds))
            return;
        final Box2D b = entry.graphic.absBound;
        entry.graphic.relAlpha = this.absAlpha;
        b.xpos(entry.xpos() + xpos() - bounds.xpos());
        b.ypos(entry.ypos() + ypos() - bounds.ypos());
        entry.graphic.updateState();
        entry.graphic.updateRelativeParent();
        entry.graphic.updateAbsoluteBounds();

        entry.graphic.render();
    }

    /**  Renders a single character within the text field, if visible.
      */
    protected boolean renderText(Box2D area, TextEntry entry, Clickable link) {
        if (entry.letter == null || !entry.intersects(area))
            return false;
        //
        //  If this text links to something, we may need to colour the text (and
        //  possibly select it's link target if clicked.)
        if (link != null && entry.link == link) {
            GL11.glColor4f(1, 1, 0, absAlpha);
        } else {
            final Colour c = entry.colour != null ? entry.colour : Colour.WHITE;
            GL11.glColor4f(c.r, c.g, c.b, c.a * absAlpha);
        }
        final float xoff = xpos() - area.xpos(), yoff = ypos() - area.ypos();
        //
        //  Draw the text entry-
        drawQuad(entry.xpos() + xoff, entry.ypos() + yoff, entry.xmax() + xoff, entry.ymax() + yoff,
                entry.letter.umin, entry.letter.vmin, entry.letter.umax, entry.letter.vmax, absDepth);
        return true;
    }

    /**  Sets this text object to the size it would ideally prefer in order to
      *  accomodate it's text.
      */
    public void setToPreferredSize(float maxWidth) {
        format(maxWidth);
        relBound.xdim(0);
        relBound.ydim(0);
        absBound.xdim(fullSize.xdim());
        absBound.ydim(fullSize.ydim());
    }

    public Box2D preferredSize() {
        return fullSize;
    }

    /**  Puts all letters in their proper place, allowing for roll/wrap/grow
      *  effects, and, if neccesary, adjusts the bounds of this UIObject
      *  accordingly.
      */
    protected void format(float maxWidth) {
        ListEntry<Box2D> open = allEntries, wordOpen = open, lineOpen = open;
        ImageEntry lastBullet = null;
        boolean newWord, newLine;
        float xpos = 0, ypos = 0;
        final float lineHigh = alphabet.map[' '].height * scale, charWide = alphabet.map[' '].width * scale;
        //
        //  Here's the main loop for determining entry positions...
        while ((open = open.nextEntry()) != allEntries) {
            newLine = newWord = false;
            //
            //  In the case of an image entry...
            if (open.refers instanceof ImageEntry) {
                if (lastBullet != null) {
                    final float minY = lastBullet.ypos() - (lineHigh * 1.5f);
                    if (ypos > minY)
                        ypos = minY;
                }
                final ImageEntry entry = (ImageEntry) open.refers;
                entry.visible = true;
                entry.set(0, ypos + lineHigh - (entry.high), entry.wide * scale, entry.high);
                xpos = entry.wide;
                lastBullet = entry;
            }
            //
            //  In the case of a text entry...
            else {
                final TextEntry entry = (TextEntry) open.refers;
                entry.visible = true;
                switch (entry.key) {
                //
                //  In the case of a return character, you definitely need a new line,
                //  and you automatically escape from the last bullet.
                //  Either that or a space means a new word.
                case ('\n'):
                    newLine = newWord = true;
                    entry.visible = false;
                    break;
                case (' '):
                    newWord = true;
                default:
                    entry.set(xpos, ypos, entry.letter.width * scale, entry.letter.height * scale);
                    xpos += entry.xdim();
                    //
                    //  If the width of the current line exceeds the space allowed, you
                    //  need to go back to that start of the current word and jump to
                    //  the next line.
                    if ((maxWidth > 0) && (xpos > maxWidth) && (wordOpen != lineOpen)) {
                        newLine = true;
                    }
                }
            }
            //
            //  Mark the first character of a new word so you can escape back to it.
            //  If a new line is called for, flow around the current bullet.
            if (newWord)
                wordOpen = open;
            if (newLine) {
                xpos = (lastBullet == null) ? 0 : (lastBullet.wide + charWide);
                ypos -= lineHigh;
                open = lineOpen = wordOpen;
            }
        }
        //
        //  We now reposition entries to fit the window, and update the full bounds.
        fullSize.set(0, 0, 0, lineHigh);
        final float heightAdjust = ydim() - lineHigh;
        for (Box2D entry : allEntries) {
            fullSize.include(entry);
            entry.ypos(entry.ypos() + heightAdjust);
        }
        needsFormat = false;
    }
}