A few days ago, I hacked clickable hyperlinks into JTA's SwingTerminal. If I were getting paid to finish a project on time, this is where I would have stopped.
But since I have no deadline, I'm spending a few days tinkering with the idea of completely rewriting the parts of JTA that I intended to use. Call it a feasibility study.
Anyway, here's the code:
package krum.weaponm.gui; import java.awt.Component; import java.awt.Cursor; import java.awt.Point; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.io.IOException; import java.io.OutputStream; import java.util.HashSet; import java.util.Set; import de.mud.terminal.SwingTerminal; import de.mud.terminal.VDUBuffer; import de.mud.terminal.vt320; /** * A wrapper around a JTA SwingTerminal and associated classes that kludges * in hover-highlighted, clickable hyperlinks in the terminal component. Link * information is piggybacked on the unused bits of character attributes stored * as 32-bit signed integers in the VDUBuffer. The standard attributes use 13 * bits, leaving 18 bits to define the link attribute. This allows up to * 262,143 link types. (A link attribute of 0 is not a link.) * * The target of a link is identical to the text of the link. In other words, * it is the contiguous sequence of characters on the same line having the * same link attribute. If two links with the same attribute are not * separated by at least one character with a different attribute, they * will merge into one link. Links may not span more than one line. * * An alternative use of the link type attribute would be to map it to the * target of the link instead of using the link text as the target. This * would allow up to 262,143 unique links, or potentially an unlimited number * if you tracked which links had scrolled out of the scrollback buffer. * Weapon M does not do this, but it might be a useful technique if anyone * ever reuses this code. * * @author Kevin Krumwiede (kjkrum@gmail.com) * @see krum.weaponm.gui.LinkListener */ public class Terminal implements MouseListener, MouseMotionListener { /** * Number of bits link type is left-shifted in character attributes. */ public static final int LINK_SHIFT = 14; /** * Mask for default character attributes. They use 13 bits. */ public static final int ATTR_MASK = 0x1FFF; /** * Maximum link attribute value. They use 18 bits. */ public static final int MAX_LINK = 0x3FFFF; /** * Normal cursor. */ private static final Cursor normalCursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR); /** * Hand cursor. */ private static final Cursor handCursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); /** * Character buffer and terminal emulation. */ private vt320 buffer; /** * Graphical representation of the terminal. */ private SwingTerminal terminal; /** * Sink for terminal output. */ private OutputStream out = null; /** * Character coordinates of the last mouse press on button 1. */ private Point lastPress = null; /** * Character coordinates of the last mouse movement event. A null value * indicates the pointer is not in the terminal component. */ private Point lastPos = null; /** * Character coordinates of the beginning of the link currently * highlighted by the mouse pointer. A null value indicates that no * highlight is currently drawn. */ private Point hoverBegin = null; /** * Length of the link currently highlighted by the mouse pointer. */ private int hoverLength = 0; /** * Listeners to receive clicks on links in terminal. */ private Set<LinkListener> linkListeners = new HashSet<LinkListener>(); public Terminal(int screenColumns, int screenRows, int bufferLines) { buffer = new vt320() { @Override public void write(byte[] b) { synchronized(Terminal.this) { if(out != null) { try { out.write(b); } catch (IOException e) { out = null; } } } } }; buffer.setBufferSize(bufferLines); terminal = new SwingTerminal(buffer); terminal.addMouseListener(this); terminal.addMouseMotionListener(this); terminal.setResizeStrategy(SwingTerminal.RESIZE_SCREEN); } public Terminal() { this(80, 25, 1000); } /** * Appends text to the terminal. The interpretation of the text and the * final cursor position will be determined by the terminal's VT320 * emulation. * * @param s */ synchronized public void append(String s) { clearHover(); buffer.putString(s); updateHover(); } synchronized public void attach(OutputStream out) { this.out = out; } public void addLinkListener(LinkListener listener) { linkListeners.add(listener); } public void removeLinkListener(LinkListener listener) { linkListeners.remove(listener); } /** * Gets the <tt>Component</tt> that represents this <tt>Terminal</tt>. * * @return */ public Component getComponent() { return terminal; } /** * Assigns a link attribute to a sequence of characters. The row and * column coordinates are relative to the top left corner of the screen. * The row number can be off the screen, as long as it is within the * scroll buffer. * * @param row zero-indexed row number; down the screen is positive * @param column zero-indexed column number * @param length number of characters in link */ public void createLink(int row, int column, int length, int type) { int absRow = row + buffer.windowBase; if( absRow < 0 || absRow >= buffer.getBufferSize() || column < 0 || length < 1 || column + length > buffer.getColumns() ) throw new IllegalArgumentException(); clearHover(); for(int i = 0; i < length; ++i) { int attr = buffer.charAttributes[absRow][column + i]; attr &= ATTR_MASK; // remove any existing link type attr += type << LINK_SHIFT; buffer.charAttributes[absRow][column + i] = attr; } updateHover(); } /** * Assigns a link attribute to a sequence of characters. The row and * column coordinates are relative to the cursor position. The row number * can be off the screen, as long as it is within the scroll buffer. * * @param row * @param column * @param length */ public void createLinkRelative(int row, int column, int length, int type) { createLink(row + buffer.getCursorRow(), column + buffer.getCursorColumn(), length, type); } /** * Gets the link type of the specified screen coordinates. * * @param row * @param column * @return */ int getLinkType(int row, int column) { int attr = buffer.charAttributes[row + buffer.windowBase][column]; return attr >> LINK_SHIFT; } void updateHover() { // if a hover is currently drawn... if(hoverBegin != null) { // find out if we are still in it if(lastPos.y == hoverBegin.y && lastPos.x >= hoverBegin.x && lastPos.x < hoverBegin.x + hoverLength) { return; } // no... so turn it off else { clearHover(); } } // find out if we moved into a new link if(lastPos == null) return; int linkType = getLinkType(lastPos.y, lastPos.x); if(linkType > 0) { // find the first column in the link int first = lastPos.x; while(first > 0 && getLinkType(lastPos.y, first - 1) == linkType) { --first; } // find the last column int last = lastPos.x; while(last < buffer.width - 1 && getLinkType(lastPos.y, last + 1) == linkType) { ++last; } // save the new hover hoverBegin = new Point(first, lastPos.y); hoverLength = last - first + 1; // draw it for(int i = 0; i < hoverLength; ++i) { int attr = buffer.charAttributes[hoverBegin.y + buffer.windowBase][hoverBegin.x + i]; attr = attr | VDUBuffer.UNDERLINE; buffer.charAttributes[hoverBegin.y + buffer.windowBase][hoverBegin.x + i] = attr; } buffer.markLine(hoverBegin.y, 1); terminal.setCursor(handCursor); terminal.redraw(); } } void clearHover() { if(hoverBegin != null) { for(int i = 0; i < hoverLength; ++i) { int attr = buffer.charAttributes[hoverBegin.y + buffer.windowBase][hoverBegin.x + i]; attr = attr & (VDUBuffer.UNDERLINE ^ Integer.MAX_VALUE); buffer.charAttributes[hoverBegin.y + buffer.windowBase][hoverBegin.x + i] = attr; } buffer.markLine(hoverBegin.y, 1); terminal.setCursor(normalCursor); terminal.redraw(); hoverBegin = null; } } @Override public void mouseExited(MouseEvent evt) { clearHover(); lastPos = null; } @Override public void mousePressed(MouseEvent evt) { if(evt.getButton() == MouseEvent.BUTTON1) { lastPress = terminal.mouseGetPos(evt.getPoint()); } } @Override public void mouseReleased(MouseEvent evt) { if(evt.getButton() == MouseEvent.BUTTON1) { Point release = terminal.mouseGetPos(evt.getPoint()); if(release.equals(lastPress)) { // if click is inside current hover if(hoverBegin != null) { if(release.y == hoverBegin.y && release.x >= hoverBegin.x && release.x < hoverBegin.x + hoverLength) { for(LinkListener l : linkListeners) { String text = new String(buffer.charArray[hoverBegin.y + buffer.windowBase], hoverBegin.x, hoverLength); int type = getLinkType(release.y, release.x); l.linkClicked(text, type); } } } } } } @Override public void mouseMoved(MouseEvent evt) { lastPos = terminal.mouseGetPos(evt.getPoint()); updateHover(); } @Override public void mouseClicked(MouseEvent arg0) { /* don't care */ } @Override public void mouseEntered(MouseEvent arg0) { /* don't care */ } @Override public void mouseDragged(MouseEvent arg0) { /* don't care */ } }
No comments:
Post a Comment