From: Kevin Lamonte Date: Mon, 7 Aug 2017 19:53:57 +0000 (-0400) Subject: #16 Refactor Swing backend, demo of multiple TApplications in one Swing frame X-Git-Url: http://git.nikiroo.be/?p=nikiroo-utils.git;a=commitdiff_plain;h=42873e30bf487bc0b695d60652dba44f82185dbb #16 Refactor Swing backend, demo of multiple TApplications in one Swing frame --- diff --git a/docs/TODO.md b/docs/TODO.md index 3f4df47..64ec854 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -10,6 +10,10 @@ BUG: TTreeView.reflow() doesn't keep the vertical dot within the 0.0.5 +- Multiscreen support: + - cmAbort to cmScreenDisconnected + - cmScreenConnected + - TApplication - getAllWindows() - Expose menu management functions (addMenu, getMenu, getAllMenus, @@ -39,12 +43,6 @@ BUG: TTreeView.reflow() doesn't keep the vertical dot within the 0.0.7 -- Refactor SwingBackend to be embeddable - - jexer.Swing.blockMousePointer: false = do not invert cell, true - (default) is current behavior - - Make Demo5 with two separate Swing demos in a single JFrame. - - Make Demo6 mixing Swing and Jexer components - - THelpWindow - TText + clickable links - Index diff --git a/docs/worklog.md b/docs/worklog.md index a169b68..2573fb2 100644 --- a/docs/worklog.md +++ b/docs/worklog.md @@ -1,6 +1,50 @@ Jexer Work Log ============== +August 7, 2017 + +Had trouble sleeping, what with a bunch of imaginative thoughts for +this release. jexer.backend will be the ultimate destination for +jexer.session and most of jexer.io. TerminalReader will be the +interface for keyboard and mouse events. cmScreenConnected and +cmScreenDisconnected will be new events to represent a screen +appearing/disappearing, and MultiBackend will be a new backend +multiplexer that goes full XRandR. Several new demos demonstrating +multi-screen support will be coming along. + +August 6, 2017 + +Time to clean up more API, particularly between Backend and Screen. +Both of these will be interfaces soon, so that one could easily +subclass JComponent and implement both Screen and Backend. The +original code evolved out of Qodem, where screen.c and input.c were +two different things leading to ECMA48Screen and ECMA48Terminal, but +now there is really no need to keep them separate. It also +complicates the constructors, as these are basically friend classes +that have used package private access to get around their artificial +separation. + +When I get this done it should be a lot easier to do any of: + +* Pass a JFrame or JComponent to SwingBackend and have it add itself, + like any other Swing widget. + +* Construct a SwingBackend and add it to any regular JComponent. + +* Have multiple TApplications running inside the same Swing + application, including having actions affect each other. (Will also + need to ensure that TWidgets/TWindows are not in different + TApplication collections.) + +* Build a Backend/Screen multiplexer, so that one could have a ECMA48 + TApplication listening on a port and a local Swing monitor for it. + +* Build a Backend/Screen manager, so that one could have multiple + ECMA48 screens acting as a single large screen (e.g. XRandR). + +Now I need to decide which package will collect Backend, SessionInfo, +and Screen. jexer.io has some java.io stuff, so it stays anyway. + July 28, 2017 Got very busy with my meatspace life, now getting a chance to come diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 3907a9d..c783d8d 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -51,16 +51,18 @@ import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; import jexer.backend.Backend; +import jexer.backend.Screen; import jexer.backend.SwingBackend; import jexer.backend.ECMA48Backend; -import jexer.io.Screen; import jexer.menu.TMenu; import jexer.menu.TMenuItem; import static jexer.TCommand.*; import static jexer.TKeypress.*; /** - * TApplication sets up a full Text User Interface application. + * TApplication is the main driver class for a full Text User Interface + * application. It manages windows, provides a menu bar and status bar, and + * processes events received from the user. */ public class TApplication implements Runnable { @@ -908,6 +910,13 @@ public class TApplication implements Runnable { // Main loop -------------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Force this application to exit. + */ + public void exit() { + quit = true; + } + /** * Run this application until it exits. */ diff --git a/src/jexer/TWidget.java b/src/jexer/TWidget.java index 5be209e..726e137 100644 --- a/src/jexer/TWidget.java +++ b/src/jexer/TWidget.java @@ -32,6 +32,7 @@ import java.io.IOException; import java.util.List; import java.util.ArrayList; +import jexer.backend.Screen; import jexer.bits.ColorTheme; import jexer.event.TCommandEvent; import jexer.event.TInputEvent; @@ -39,7 +40,6 @@ import jexer.event.TKeypressEvent; import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; -import jexer.io.Screen; import jexer.menu.TMenu; import static jexer.TKeypress.*; diff --git a/src/jexer/TWindow.java b/src/jexer/TWindow.java index 8749eb8..8032fdc 100644 --- a/src/jexer/TWindow.java +++ b/src/jexer/TWindow.java @@ -30,6 +30,7 @@ package jexer; import java.util.HashSet; +import jexer.backend.Screen; import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.bits.GraphicsChars; @@ -38,7 +39,6 @@ import jexer.event.TKeypressEvent; import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; -import jexer.io.Screen; import jexer.menu.TMenu; import static jexer.TCommand.*; import static jexer.TKeypress.*; diff --git a/src/jexer/backend/Backend.java b/src/jexer/backend/Backend.java index 664dff9..2793ab0 100644 --- a/src/jexer/backend/Backend.java +++ b/src/jexer/backend/Backend.java @@ -31,69 +31,54 @@ package jexer.backend; import java.util.List; import jexer.event.TInputEvent; -import jexer.io.Screen; -import jexer.session.SessionInfo; /** - * This abstract class provides a screen, keyboard, and mouse to - * TApplication. It also exposes session information as gleaned from lower - * levels of the communication stack. + * This interface provides a screen, keyboard, and mouse to TApplication. It + * also exposes session information as gleaned from lower levels of the + * communication stack. */ -public abstract class Backend { +public interface Backend { /** - * The session information. - */ - protected SessionInfo sessionInfo; - - /** - * Getter for sessionInfo. + * Get a SessionInfo, which exposes text width/height, language, + * username, and other information from the communication stack. * * @return the SessionInfo */ - public final SessionInfo getSessionInfo() { - return sessionInfo; - } - - /** - * The screen to draw on. - */ - protected Screen screen; + public SessionInfo getSessionInfo(); /** - * Getter for screen. + * Get a Screen, which displays the text cells to the user. * * @return the Screen */ - public final Screen getScreen() { - return screen; - } + public Screen getScreen(); /** - * Subclasses must provide an implementation that syncs the logical - * screen to the physical device. + * Classes must provide an implementation that syncs the logical screen + * to the physical device. */ - public abstract void flushScreen(); + public void flushScreen(); /** - * Subclasses must provide an implementation to get keyboard, mouse, and + * Classes must provide an implementation to get keyboard, mouse, and * screen resize events. * * @param queue list to append new events to */ - public abstract void getEvents(List queue); + public void getEvents(List queue); /** - * Subclasses must provide an implementation that closes sockets, - * restores console, etc. + * Classes must provide an implementation that closes sockets, restores + * console, etc. */ - public abstract void shutdown(); + public void shutdown(); /** - * Subclasses must provide an implementation that sets the window title. + * Classes must provide an implementation that sets the window title. * * @param title the new title */ - public abstract void setTitle(final String title); + public void setTitle(final String title); } diff --git a/src/jexer/backend/ECMA48Backend.java b/src/jexer/backend/ECMA48Backend.java index 24357bb..ee7a103 100644 --- a/src/jexer/backend/ECMA48Backend.java +++ b/src/jexer/backend/ECMA48Backend.java @@ -36,14 +36,12 @@ import java.io.UnsupportedEncodingException; import java.util.List; import jexer.event.TInputEvent; -import jexer.io.ECMA48Screen; -import jexer.io.ECMA48Terminal; /** * This class uses an xterm/ANSI X3.64/ECMA-48 type terminal to provide a * screen, keyboard, and mouse to TApplication. */ -public final class ECMA48Backend extends Backend { +public final class ECMA48Backend extends GenericBackend { /** * Input events are processed by this Terminal. @@ -74,12 +72,8 @@ public final class ECMA48Backend extends Backend { // Keep the terminal's sessionInfo so that TApplication can see it sessionInfo = terminal.getSessionInfo(); - // Create a screen - screen = new ECMA48Screen(terminal); - - // Clear the screen - terminal.getOutput().write(terminal.clearAll()); - terminal.flush(); + // ECMA48Terminal is the screen too + screen = terminal; } /** @@ -107,12 +101,8 @@ public final class ECMA48Backend extends Backend { // Keep the terminal's sessionInfo so that TApplication can see it sessionInfo = terminal.getSessionInfo(); - // Create a screen - screen = new ECMA48Screen(terminal); - - // Clear the screen - terminal.getOutput().write(terminal.clearAll()); - terminal.flush(); + // ECMA48Terminal is the screen too + screen = terminal; } /** @@ -157,7 +147,7 @@ public final class ECMA48Backend extends Backend { */ @Override public void shutdown() { - terminal.shutdown(); + terminal.closeTerminal(); } /** @@ -167,7 +157,7 @@ public final class ECMA48Backend extends Backend { */ @Override public void setTitle(final String title) { - ((ECMA48Screen) screen).setTitle(title); + screen.setTitle(title); } } diff --git a/src/jexer/io/ECMA48Terminal.java b/src/jexer/backend/ECMA48Terminal.java similarity index 82% rename from src/jexer/io/ECMA48Terminal.java rename to src/jexer/backend/ECMA48Terminal.java index 0b1ce11..1370415 100644 --- a/src/jexer/io/ECMA48Terminal.java +++ b/src/jexer/backend/ECMA48Terminal.java @@ -26,7 +26,7 @@ * @author Kevin Lamonte [kevin.lamonte@gmail.com] * @version 1 */ -package jexer.io; +package jexer.backend; import java.io.BufferedReader; import java.io.FileDescriptor; @@ -44,21 +44,26 @@ import java.util.Date; import java.util.List; import java.util.LinkedList; +import jexer.bits.Cell; +import jexer.bits.CellAttributes; import jexer.bits.Color; import jexer.event.TInputEvent; import jexer.event.TKeypressEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; -import jexer.session.SessionInfo; -import jexer.session.TSessionInfo; -import jexer.session.TTYSessionInfo; import static jexer.TKeypress.*; /** * This class reads keystrokes and mouse events and emits output to ANSI * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc. */ -public final class ECMA48Terminal implements Runnable { +public final class ECMA48Terminal extends LogicalScreen + implements TerminalReader, Runnable { + + /** + * Emit debugging to stderr. + */ + private boolean debugToStderr = false; /** * If true, emit T.416-style RGB colors. This is a) expensive in @@ -294,7 +299,7 @@ public final class ECMA48Terminal implements Runnable { public ECMA48Terminal(final Object listener, final InputStream input, final OutputStream output) throws UnsupportedEncodingException { - reset(); + resetParser(); mouse1 = false; mouse2 = false; mouse3 = false; @@ -352,6 +357,14 @@ public final class ECMA48Terminal implements Runnable { eventQueue = new LinkedList(); readerThread = new Thread(this); readerThread.start(); + + // Query the screen size + setDimensions(sessionInfo.getWindowWidth(), + sessionInfo.getWindowHeight()); + + // Clear the screen + this.output.write(clearAll()); + this.output.flush(); } /** @@ -381,7 +394,7 @@ public final class ECMA48Terminal implements Runnable { if (writer == null) { throw new IllegalArgumentException("Writer must be specified"); } - reset(); + resetParser(); mouse1 = false; mouse2 = false; mouse3 = false; @@ -431,6 +444,14 @@ public final class ECMA48Terminal implements Runnable { eventQueue = new LinkedList(); readerThread = new Thread(this); readerThread.start(); + + // Query the screen size + setDimensions(sessionInfo.getWindowWidth(), + sessionInfo.getWindowHeight()); + + // Clear the screen + this.output.write(clearAll()); + this.output.flush(); } /** @@ -453,7 +474,7 @@ public final class ECMA48Terminal implements Runnable { /** * Restore terminal to normal state. */ - public void shutdown() { + public void closeTerminal() { // System.err.println("=== shutdown() ==="); System.err.flush(); @@ -498,10 +519,243 @@ public final class ECMA48Terminal implements Runnable { output.flush(); } + /** + * Perform a somewhat-optimal rendering of a line. + * + * @param y row coordinate. 0 is the top-most row. + * @param sb StringBuilder to write escape sequences to + * @param lastAttr cell attributes from the last call to flushLine + */ + private void flushLine(final int y, final StringBuilder sb, + CellAttributes lastAttr) { + + int lastX = -1; + int textEnd = 0; + for (int x = 0; x < width; x++) { + Cell lCell = logical[x][y]; + if (!lCell.isBlank()) { + textEnd = x; + } + } + // Push textEnd to first column beyond the text area + textEnd++; + + // DEBUG + // reallyCleared = true; + + for (int x = 0; x < width; x++) { + Cell lCell = logical[x][y]; + Cell pCell = physical[x][y]; + + if (!lCell.equals(pCell) || reallyCleared) { + + if (debugToStderr) { + System.err.printf("\n--\n"); + System.err.printf(" Y: %d X: %d\n", y, x); + System.err.printf(" lCell: %s\n", lCell); + System.err.printf(" pCell: %s\n", pCell); + System.err.printf(" ==== \n"); + } + + if (lastAttr == null) { + lastAttr = new CellAttributes(); + sb.append(normal()); + } + + // Place the cell + if ((lastX != (x - 1)) || (lastX == -1)) { + // Advancing at least one cell, or the first gotoXY + sb.append(gotoXY(x, y)); + } + + assert (lastAttr != null); + + if ((x == textEnd) && (textEnd < width - 1)) { + assert (lCell.isBlank()); + + for (int i = x; i < width; i++) { + assert (logical[i][y].isBlank()); + // Physical is always updated + physical[i][y].reset(); + } + + // Clear remaining line + sb.append(clearRemainingLine()); + lastAttr.reset(); + return; + } + + // Now emit only the modified attributes + if ((lCell.getForeColor() != lastAttr.getForeColor()) + && (lCell.getBackColor() != lastAttr.getBackColor()) + && (lCell.isBold() == lastAttr.isBold()) + && (lCell.isReverse() == lastAttr.isReverse()) + && (lCell.isUnderline() == lastAttr.isUnderline()) + && (lCell.isBlink() == lastAttr.isBlink()) + ) { + // Both colors changed, attributes the same + sb.append(color(lCell.isBold(), + lCell.getForeColor(), lCell.getBackColor())); + + if (debugToStderr) { + System.err.printf("1 Change only fore/back colors\n"); + } + } else if ((lCell.getForeColor() != lastAttr.getForeColor()) + && (lCell.getBackColor() != lastAttr.getBackColor()) + && (lCell.isBold() != lastAttr.isBold()) + && (lCell.isReverse() != lastAttr.isReverse()) + && (lCell.isUnderline() != lastAttr.isUnderline()) + && (lCell.isBlink() != lastAttr.isBlink()) + ) { + // Everything is different + sb.append(color(lCell.getForeColor(), + lCell.getBackColor(), + lCell.isBold(), lCell.isReverse(), + lCell.isBlink(), + lCell.isUnderline())); + + if (debugToStderr) { + System.err.printf("2 Set all attributes\n"); + } + } else if ((lCell.getForeColor() != lastAttr.getForeColor()) + && (lCell.getBackColor() == lastAttr.getBackColor()) + && (lCell.isBold() == lastAttr.isBold()) + && (lCell.isReverse() == lastAttr.isReverse()) + && (lCell.isUnderline() == lastAttr.isUnderline()) + && (lCell.isBlink() == lastAttr.isBlink()) + ) { + + // Attributes same, foreColor different + sb.append(color(lCell.isBold(), + lCell.getForeColor(), true)); + + if (debugToStderr) { + System.err.printf("3 Change foreColor\n"); + } + } else if ((lCell.getForeColor() == lastAttr.getForeColor()) + && (lCell.getBackColor() != lastAttr.getBackColor()) + && (lCell.isBold() == lastAttr.isBold()) + && (lCell.isReverse() == lastAttr.isReverse()) + && (lCell.isUnderline() == lastAttr.isUnderline()) + && (lCell.isBlink() == lastAttr.isBlink()) + ) { + // Attributes same, backColor different + sb.append(color(lCell.isBold(), + lCell.getBackColor(), false)); + + if (debugToStderr) { + System.err.printf("4 Change backColor\n"); + } + } else if ((lCell.getForeColor() == lastAttr.getForeColor()) + && (lCell.getBackColor() == lastAttr.getBackColor()) + && (lCell.isBold() == lastAttr.isBold()) + && (lCell.isReverse() == lastAttr.isReverse()) + && (lCell.isUnderline() == lastAttr.isUnderline()) + && (lCell.isBlink() == lastAttr.isBlink()) + ) { + + // All attributes the same, just print the char + // NOP + + if (debugToStderr) { + System.err.printf("5 Only emit character\n"); + } + } else { + // Just reset everything again + sb.append(color(lCell.getForeColor(), + lCell.getBackColor(), + lCell.isBold(), + lCell.isReverse(), + lCell.isBlink(), + lCell.isUnderline())); + + if (debugToStderr) { + System.err.printf("6 Change all attributes\n"); + } + } + // Emit the character + sb.append(lCell.getChar()); + + // Save the last rendered cell + lastX = x; + lastAttr.setTo(lCell); + + // Physical is always updated + physical[x][y].setTo(lCell); + + } // if (!lCell.equals(pCell) || (reallyCleared == true)) + + } // for (int x = 0; x < width; x++) + } + + /** + * Render the screen to a string that can be emitted to something that + * knows how to process ECMA-48/ANSI X3.64 escape sequences. + * + * @return escape sequences string that provides the updates to the + * physical screen + */ + private String flushString() { + if (!dirty) { + assert (!reallyCleared); + return ""; + } + + CellAttributes attr = null; + + StringBuilder sb = new StringBuilder(); + if (reallyCleared) { + attr = new CellAttributes(); + sb.append(clearAll()); + } + + for (int y = 0; y < height; y++) { + flushLine(y, sb, attr); + } + + dirty = false; + reallyCleared = false; + + String result = sb.toString(); + if (debugToStderr) { + System.err.printf("flushString(): %s\n", result); + } + return result; + } + + /** + * Push the logical screen to the physical device. + */ + @Override + public void flushPhysical() { + String result = flushString(); + if ((cursorVisible) + && (cursorY <= height - 1) + && (cursorX <= width - 1) + ) { + result += cursor(true); + result += gotoXY(cursorX, cursorY); + } else { + result += cursor(false); + } + output.write(result); + flush(); + } + + /** + * Set the window title. + * + * @param title the new title + */ + public void setTitle(final String title) { + output.write(getSetTitleString(title)); + flush(); + } + /** * Reset keyboard/mouse input parser. */ - private void reset() { + private void resetParser() { state = ParseState.GROUND; params = new ArrayList(); params.clear(); @@ -860,7 +1114,7 @@ public final class ECMA48Terminal implements Runnable { if (escDelay > 100) { // After 0.1 seconds, assume a true escape character queue.add(controlChar((char)0x1B, false)); - reset(); + resetParser(); } } } @@ -926,7 +1180,7 @@ public final class ECMA48Terminal implements Runnable { if (escDelay > 250) { // After 0.25 seconds, assume a true escape character events.add(controlChar((char)0x1B, false)); - reset(); + resetParser(); } } @@ -949,7 +1203,7 @@ public final class ECMA48Terminal implements Runnable { if (ch <= 0x1F) { // Control character events.add(controlChar(ch, false)); - reset(); + resetParser(); return; } @@ -957,7 +1211,7 @@ public final class ECMA48Terminal implements Runnable { // Normal character events.add(new TKeypressEvent(false, 0, ch, false, false, false)); - reset(); + resetParser(); return; } @@ -967,7 +1221,7 @@ public final class ECMA48Terminal implements Runnable { if (ch <= 0x1F) { // ALT-Control character events.add(controlChar(ch, true)); - reset(); + resetParser(); return; } @@ -989,7 +1243,7 @@ public final class ECMA48Terminal implements Runnable { } alt = true; events.add(new TKeypressEvent(false, 0, ch, alt, ctrl, shift)); - reset(); + resetParser(); return; case ESCAPE_INTERMEDIATE: @@ -1011,12 +1265,12 @@ public final class ECMA48Terminal implements Runnable { default: break; } - reset(); + resetParser(); return; } // Unknown keystroke, ignore - reset(); + resetParser(); return; case CSI_ENTRY: @@ -1038,37 +1292,37 @@ public final class ECMA48Terminal implements Runnable { case 'A': // Up events.add(new TKeypressEvent(kbUp, alt, ctrl, shift)); - reset(); + resetParser(); return; case 'B': // Down events.add(new TKeypressEvent(kbDown, alt, ctrl, shift)); - reset(); + resetParser(); return; case 'C': // Right events.add(new TKeypressEvent(kbRight, alt, ctrl, shift)); - reset(); + resetParser(); return; case 'D': // Left events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift)); - reset(); + resetParser(); return; case 'H': // Home events.add(new TKeypressEvent(kbHome)); - reset(); + resetParser(); return; case 'F': // End events.add(new TKeypressEvent(kbEnd)); - reset(); + resetParser(); return; case 'Z': // CBT - Cursor backward X tab stops (default 1) events.add(new TKeypressEvent(kbBackTab)); - reset(); + resetParser(); return; case 'M': // Mouse position @@ -1084,7 +1338,7 @@ public final class ECMA48Terminal implements Runnable { } // Unknown keystroke, ignore - reset(); + resetParser(); return; case MOUSE_SGR: @@ -1107,7 +1361,7 @@ public final class ECMA48Terminal implements Runnable { if (event != null) { events.add(event); } - reset(); + resetParser(); return; case 'm': // Generate a mouse release event @@ -1115,14 +1369,14 @@ public final class ECMA48Terminal implements Runnable { if (event != null) { events.add(event); } - reset(); + resetParser(); return; default: break; } // Unknown keystroke, ignore - reset(); + resetParser(); return; case CSI_PARAM: @@ -1141,7 +1395,7 @@ public final class ECMA48Terminal implements Runnable { if (ch == '~') { events.add(csiFnKey()); - reset(); + resetParser(); return; } @@ -1155,7 +1409,7 @@ public final class ECMA48Terminal implements Runnable { ctrl = csiIsCtrl(params.get(1)); } events.add(new TKeypressEvent(kbUp, alt, ctrl, shift)); - reset(); + resetParser(); return; case 'B': // Down @@ -1165,7 +1419,7 @@ public final class ECMA48Terminal implements Runnable { ctrl = csiIsCtrl(params.get(1)); } events.add(new TKeypressEvent(kbDown, alt, ctrl, shift)); - reset(); + resetParser(); return; case 'C': // Right @@ -1175,7 +1429,7 @@ public final class ECMA48Terminal implements Runnable { ctrl = csiIsCtrl(params.get(1)); } events.add(new TKeypressEvent(kbRight, alt, ctrl, shift)); - reset(); + resetParser(); return; case 'D': // Left @@ -1185,7 +1439,7 @@ public final class ECMA48Terminal implements Runnable { ctrl = csiIsCtrl(params.get(1)); } events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift)); - reset(); + resetParser(); return; case 'H': // Home @@ -1195,7 +1449,7 @@ public final class ECMA48Terminal implements Runnable { ctrl = csiIsCtrl(params.get(1)); } events.add(new TKeypressEvent(kbHome, alt, ctrl, shift)); - reset(); + resetParser(); return; case 'F': // End @@ -1205,7 +1459,7 @@ public final class ECMA48Terminal implements Runnable { ctrl = csiIsCtrl(params.get(1)); } events.add(new TKeypressEvent(kbEnd, alt, ctrl, shift)); - reset(); + resetParser(); return; default: break; @@ -1213,7 +1467,7 @@ public final class ECMA48Terminal implements Runnable { } // Unknown keystroke, ignore - reset(); + resetParser(); return; case MOUSE: @@ -1221,7 +1475,7 @@ public final class ECMA48Terminal implements Runnable { if (params.get(0).length() == 3) { // We have enough to generate a mouse event events.add(parseMouse()); - reset(); + resetParser(); } return; @@ -1249,19 +1503,17 @@ public final class ECMA48Terminal implements Runnable { } /** - * Create an xterm OSC sequence to change the window title. Note package - * private access. + * Create an xterm OSC sequence to change the window title. * * @param title the new title * @return the string to emit to xterm */ - String setTitle(final String title) { + private String getSetTitleString(final String title) { return "\033]2;" + title + "\007"; } /** - * Create a SGR parameter sequence for a single color change. Note - * package private access. + * Create a SGR parameter sequence for a single color change. * * @param bold if true, set bold * @param color one of the Color.WHITE, Color.BLUE, etc. constants @@ -1269,7 +1521,7 @@ public final class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[42m" */ - String color(final boolean bold, final Color color, + private String color(final boolean bold, final Color color, final boolean foreground) { return color(color, foreground, true) + rgbColor(bold, color, foreground); @@ -1389,7 +1641,7 @@ public final class ECMA48Terminal implements Runnable { /** * Create a SGR parameter sequence for both foreground and background - * color change. Note package private access. + * color change. * * @param bold if true, set bold * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants @@ -1397,7 +1649,7 @@ public final class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[31;42m" */ - String color(final boolean bold, final Color foreColor, + private String color(final boolean bold, final Color foreColor, final Color backColor) { return color(foreColor, backColor, true) + rgbColor(bold, foreColor, backColor); @@ -1434,8 +1686,7 @@ public final class ECMA48Terminal implements Runnable { /** * Create a SGR parameter sequence for foreground, background, and * several attributes. This sequence first resets all attributes to - * default, then sets attributes as per the parameters. Note package - * private access. + * default, then sets attributes as per the parameters. * * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants @@ -1446,7 +1697,7 @@ public final class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[0;1;31;42m" */ - String color(final Color foreColor, final Color backColor, + private String color(final Color foreColor, final Color backColor, final boolean bold, final boolean reverse, final boolean blink, final boolean underline) { @@ -1498,13 +1749,12 @@ public final class ECMA48Terminal implements Runnable { } /** - * Create a SGR parameter sequence to reset to defaults. Note package - * private access. + * Create a SGR parameter sequence to reset to defaults. * * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[0m" */ - String normal() { + private String normal() { return normal(true) + rgbColor(false, Color.WHITE, Color.BLACK); } @@ -1524,13 +1774,12 @@ public final class ECMA48Terminal implements Runnable { } /** - * Create a SGR parameter sequence for enabling the visible cursor. Note - * package private access. + * Create a SGR parameter sequence for enabling the visible cursor. * * @param on if true, turn on cursor * @return the string to emit to an ANSI / ECMA-style terminal */ - String cursor(final boolean on) { + private String cursor(final boolean on) { if (on && !cursorOn) { cursorOn = true; return "\033[?25h"; @@ -1548,29 +1797,29 @@ public final class ECMA48Terminal implements Runnable { * * @return the string to emit to an ANSI / ECMA-style terminal */ - public String clearAll() { + private String clearAll() { return "\033[0;37;40m\033[2J"; } /** * Clear the line from the cursor (inclusive) to the end of the screen. * Because some terminals use back-color-erase, set the color to - * white-on-black beforehand. Note package private access. + * white-on-black beforehand. * * @return the string to emit to an ANSI / ECMA-style terminal */ - String clearRemainingLine() { + private String clearRemainingLine() { return "\033[0;37;40m\033[K"; } /** - * Move the cursor to (x, y). Note package private access. + * Move the cursor to (x, y). * * @param x column coordinate. 0 is the left-most column. * @param y row coordinate. 0 is the top-most row. * @return the string to emit to an ANSI / ECMA-style terminal */ - String gotoXY(final int x, final int y) { + private String gotoXY(final int x, final int y) { return String.format("\033[%d;%dH", y + 1, x + 1); } diff --git a/src/jexer/backend/GenericBackend.java b/src/jexer/backend/GenericBackend.java new file mode 100644 index 0000000..d96f7a9 --- /dev/null +++ b/src/jexer/backend/GenericBackend.java @@ -0,0 +1,97 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.backend; + +import java.util.List; + +import jexer.event.TInputEvent; + +/** + * This abstract class provides a screen, keyboard, and mouse to + * TApplication. It also exposes session information as gleaned from lower + * levels of the communication stack. + */ +public abstract class GenericBackend implements Backend { + + /** + * The session information. + */ + protected SessionInfo sessionInfo; + + /** + * Getter for sessionInfo. + * + * @return the SessionInfo + */ + public final SessionInfo getSessionInfo() { + return sessionInfo; + } + + /** + * The screen to draw on. + */ + protected Screen screen; + + /** + * Getter for screen. + * + * @return the Screen + */ + public final Screen getScreen() { + return screen; + } + + /** + * Subclasses must provide an implementation that syncs the logical + * screen to the physical device. + */ + public abstract void flushScreen(); + + /** + * Subclasses must provide an implementation to get keyboard, mouse, and + * screen resize events. + * + * @param queue list to append new events to + */ + public abstract void getEvents(List queue); + + /** + * Subclasses must provide an implementation that closes sockets, + * restores console, etc. + */ + public abstract void shutdown(); + + /** + * Subclasses must provide an implementation that sets the window title. + * + * @param title the new title + */ + public abstract void setTitle(final String title); + +} diff --git a/src/jexer/io/Screen.java b/src/jexer/backend/LogicalScreen.java similarity index 98% rename from src/jexer/io/Screen.java rename to src/jexer/backend/LogicalScreen.java index 85e121f..8432e4e 100644 --- a/src/jexer/io/Screen.java +++ b/src/jexer/backend/LogicalScreen.java @@ -26,17 +26,16 @@ * @author Kevin Lamonte [kevin.lamonte@gmail.com] * @version 1 */ -package jexer.io; +package jexer.backend; import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.bits.GraphicsChars; /** - * This class represents a text-based screen. Drawing operations write to a - * logical screen. + * A logical screen composed of a 2D array of Cells. */ -public abstract class Screen { +public class LogicalScreen implements Screen { /** * Width of the visible window. @@ -551,7 +550,7 @@ public abstract class Screen { /** * Public constructor. Sets everything to not-bold, white-on-black. */ - protected Screen() { + protected LogicalScreen() { offsetX = 0; offsetY = 0; width = 80; @@ -747,10 +746,9 @@ public abstract class Screen { } /** - * Subclasses must provide an implementation to push the logical screen - * to the physical device. + * Default implementation does nothing. */ - public abstract void flushPhysical(); + public void flushPhysical() {} /** * Put the cursor at (x,y). @@ -772,4 +770,12 @@ public abstract class Screen { public final void hideCursor() { cursorVisible = false; } + + /** + * Set the window title. Default implementation does nothing. + * + * @param title the new title + */ + public void setTitle(final String title) {} + } diff --git a/src/jexer/backend/Screen.java b/src/jexer/backend/Screen.java new file mode 100644 index 0000000..9b5890f --- /dev/null +++ b/src/jexer/backend/Screen.java @@ -0,0 +1,355 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.backend; + +import jexer.bits.Cell; +import jexer.bits.CellAttributes; + +/** + * Drawing operations API. + */ +public interface Screen { + + /** + * Set drawing offset for x. + * + * @param offsetX new drawing offset + */ + public void setOffsetX(final int offsetX); + + /** + * Set drawing offset for y. + * + * @param offsetY new drawing offset + */ + public void setOffsetY(final int offsetY); + + /** + * Get right drawing clipping boundary. + * + * @return drawing boundary + */ + public int getClipRight(); + + /** + * Set right drawing clipping boundary. + * + * @param clipRight new boundary + */ + public void setClipRight(final int clipRight); + + /** + * Get bottom drawing clipping boundary. + * + * @return drawing boundary + */ + public int getClipBottom(); + + /** + * Set bottom drawing clipping boundary. + * + * @param clipBottom new boundary + */ + public void setClipBottom(final int clipBottom); + + /** + * Get left drawing clipping boundary. + * + * @return drawing boundary + */ + public int getClipLeft(); + + /** + * Set left drawing clipping boundary. + * + * @param clipLeft new boundary + */ + public void setClipLeft(final int clipLeft); + + /** + * Get top drawing clipping boundary. + * + * @return drawing boundary + */ + public int getClipTop(); + + /** + * Set top drawing clipping boundary. + * + * @param clipTop new boundary + */ + public void setClipTop(final int clipTop); + + /** + * Get dirty flag. + * + * @return if true, the logical screen is not in sync with the physical + * screen + */ + public boolean isDirty(); + + /** + * Get the attributes at one location. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @return attributes at (x, y) + */ + public CellAttributes getAttrXY(final int x, final int y); + + /** + * Set the attributes at one location. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param attr attributes to use (bold, foreColor, backColor) + */ + public void putAttrXY(final int x, final int y, + final CellAttributes attr); + + /** + * Set the attributes at one location. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param attr attributes to use (bold, foreColor, backColor) + * @param clip if true, honor clipping/offset + */ + public void putAttrXY(final int x, final int y, + final CellAttributes attr, final boolean clip); + + /** + * Fill the entire screen with one character with attributes. + * + * @param ch character to draw + * @param attr attributes to use (bold, foreColor, backColor) + */ + public void putAll(final char ch, final CellAttributes attr); + + /** + * Render one character with attributes. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param ch character + attributes to draw + */ + public void putCharXY(final int x, final int y, final Cell ch); + + /** + * Render one character with attributes. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param ch character to draw + * @param attr attributes to use (bold, foreColor, backColor) + */ + public void putCharXY(final int x, final int y, final char ch, + final CellAttributes attr); + + /** + * Render one character without changing the underlying attributes. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param ch character to draw + */ + public void putCharXY(final int x, final int y, final char ch); + + /** + * Render a string. Does not wrap if the string exceeds the line. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param str string to draw + * @param attr attributes to use (bold, foreColor, backColor) + */ + public void putStringXY(final int x, final int y, final String str, + final CellAttributes attr); + + /** + * Render a string without changing the underlying attribute. Does not + * wrap if the string exceeds the line. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param str string to draw + */ + public void putStringXY(final int x, final int y, final String str); + + /** + * Draw a vertical line from (x, y) to (x, y + n). + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param n number of characters to draw + * @param ch character to draw + * @param attr attributes to use (bold, foreColor, backColor) + */ + public void vLineXY(final int x, final int y, final int n, + final char ch, final CellAttributes attr); + + /** + * Draw a horizontal line from (x, y) to (x + n, y). + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param n number of characters to draw + * @param ch character to draw + * @param attr attributes to use (bold, foreColor, backColor) + */ + public void hLineXY(final int x, final int y, final int n, + final char ch, final CellAttributes attr); + + /** + * Change the width. Everything on-screen will be destroyed and must be + * redrawn. + * + * @param width new screen width + */ + public void setWidth(final int width); + + /** + * Change the height. Everything on-screen will be destroyed and must be + * redrawn. + * + * @param height new screen height + */ + public void setHeight(final int height); + + /** + * Change the width and height. Everything on-screen will be destroyed + * and must be redrawn. + * + * @param width new screen width + * @param height new screen height + */ + public void setDimensions(final int width, final int height); + + /** + * Get the height. + * + * @return current screen height + */ + public int getHeight(); + + /** + * Get the width. + * + * @return current screen width + */ + public int getWidth(); + + /** + * Reset screen to not-bold, white-on-black. Also flushes the offset and + * clip variables. + */ + public void reset(); + + /** + * Flush the offset and clip variables. + */ + public void resetClipping(); + + /** + * Clear the logical screen. + */ + public void clear(); + + /** + * Draw a box with a border and empty background. + * + * @param left left column of box. 0 is the left-most row. + * @param top top row of the box. 0 is the top-most row. + * @param right right column of box + * @param bottom bottom row of the box + * @param border attributes to use for the border + * @param background attributes to use for the background + */ + public void drawBox(final int left, final int top, + final int right, final int bottom, + final CellAttributes border, final CellAttributes background); + + /** + * Draw a box with a border and empty background. + * + * @param left left column of box. 0 is the left-most row. + * @param top top row of the box. 0 is the top-most row. + * @param right right column of box + * @param bottom bottom row of the box + * @param border attributes to use for the border + * @param background attributes to use for the background + * @param borderType if 1, draw a single-line border; if 2, draw a + * double-line border; if 3, draw double-line top/bottom edges and + * single-line left/right edges (like Qmodem) + * @param shadow if true, draw a "shadow" on the box + */ + public void drawBox(final int left, final int top, + final int right, final int bottom, + final CellAttributes border, final CellAttributes background, + final int borderType, final boolean shadow); + + /** + * Draw a box shadow. + * + * @param left left column of box. 0 is the left-most row. + * @param top top row of the box. 0 is the top-most row. + * @param right right column of box + * @param bottom bottom row of the box + */ + public void drawBoxShadow(final int left, final int top, + final int right, final int bottom); + + /** + * Classes must provide an implementation to push the logical screen to + * the physical device. + */ + public void flushPhysical(); + + /** + * Put the cursor at (x,y). + * + * @param visible if true, the cursor should be visible + * @param x column coordinate to put the cursor on + * @param y row coordinate to put the cursor on + */ + public void putCursor(final boolean visible, final int x, final int y); + + /** + * Hide the cursor. + */ + public void hideCursor(); + + /** + * Set the window title. + * + * @param title the new title + */ + public void setTitle(final String title); + +} diff --git a/src/jexer/session/SessionInfo.java b/src/jexer/backend/SessionInfo.java similarity index 98% rename from src/jexer/session/SessionInfo.java rename to src/jexer/backend/SessionInfo.java index 0712b6a..5c2d034 100644 --- a/src/jexer/session/SessionInfo.java +++ b/src/jexer/backend/SessionInfo.java @@ -26,7 +26,7 @@ * @author Kevin Lamonte [kevin.lamonte@gmail.com] * @version 1 */ -package jexer.session; +package jexer.backend; /** * SessionInfo is used to store per-session properties that are determined at diff --git a/src/jexer/backend/SwingBackend.java b/src/jexer/backend/SwingBackend.java index acca7fa..876015e 100644 --- a/src/jexer/backend/SwingBackend.java +++ b/src/jexer/backend/SwingBackend.java @@ -29,16 +29,15 @@ package jexer.backend; import java.util.List; +import javax.swing.JComponent; import jexer.event.TInputEvent; -import jexer.io.SwingScreen; -import jexer.io.SwingTerminal; /** * This class uses standard Swing calls to handle screen, keyboard, and mouse * I/O. */ -public final class SwingBackend extends Backend { +public final class SwingBackend extends GenericBackend { /** * Input events are processed by this Terminal. @@ -56,7 +55,7 @@ public final class SwingBackend extends Backend { } /** - * Public constructor. + * Public constructor will spawn a new JFrame. * * @param listener the object this backend needs to wake up when new * input comes in @@ -68,16 +67,40 @@ public final class SwingBackend extends Backend { public SwingBackend(final Object listener, final int windowWidth, final int windowHeight, final int fontSize) { - // Create a screen - SwingScreen screen = new SwingScreen(windowWidth, windowHeight, - fontSize); - this.screen = screen; + // Create a Swing backend using a JFrame + terminal = new SwingTerminal(windowWidth, windowHeight, fontSize, + listener); - // Create the Swing event listeners - terminal = new SwingTerminal(listener, screen); + // Hang onto the session info + this.sessionInfo = terminal.getSessionInfo(); + + // SwingTerminal is the screen too + screen = terminal; + } + + /** + * Public constructor will render onto a JComponent. + * + * @param component the Swing component to render to + * @param listener the object this backend needs to wake up when new + * input comes in + * @param windowWidth the number of text columns to start with + * @param windowHeight the number of text rows to start with + * @param fontSize the size in points. Good values to pick are: 16, 20, + * 22, and 24. + */ + public SwingBackend(final JComponent component, final Object listener, + final int windowWidth, final int windowHeight, final int fontSize) { + + // Create a Swing backend using a JComponent + terminal = new SwingTerminal(component, windowWidth, windowHeight, + fontSize, listener); // Hang onto the session info this.sessionInfo = terminal.getSessionInfo(); + + // SwingTerminal is the screen too + screen = terminal; } /** @@ -105,7 +128,7 @@ public final class SwingBackend extends Backend { */ @Override public void shutdown() { - ((SwingScreen) screen).shutdown(); + terminal.closeTerminal(); } /** @@ -115,7 +138,17 @@ public final class SwingBackend extends Backend { */ @Override public void setTitle(final String title) { - ((SwingScreen) screen).setTitle(title); + screen.setTitle(title); + } + + /** + * Set listener to a different Object. + * + * @param listener the new listening object that run() wakes up on new + * input + */ + public void setListener(final Object listener) { + terminal.setListener(listener); } } diff --git a/src/jexer/backend/SwingComponent.java b/src/jexer/backend/SwingComponent.java new file mode 100644 index 0000000..84e1472 --- /dev/null +++ b/src/jexer/backend/SwingComponent.java @@ -0,0 +1,438 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.backend; + +import java.awt.Color; +import java.awt.Cursor; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Insets; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.ComponentListener; +import java.awt.event.KeyListener; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowListener; +import java.awt.image.BufferedImage; +import java.awt.image.BufferStrategy; +import javax.swing.JComponent; +import javax.swing.JFrame; + +/** + * Wrapper for integrating with Swing, because JFrame and JComponent have + * separate hierarchies. + */ +class SwingComponent { + + /** + * If true, use triple buffering when drawing to a JFrame. + */ + public static boolean tripleBuffer = false; + + /** + * Get the BufferStrategy object needed for triple-buffering. + * + * @return the BufferStrategy + * @throws IllegalArgumentException if this function is called when + * not rendering to a JFrame + */ + public BufferStrategy getBufferStrategy() { + if (frame != null) { + return frame.getBufferStrategy(); + } else { + throw new IllegalArgumentException("BufferStrategy not used " + + "for JComponent access"); + } + } + + /** + * The frame reference, if we are drawing to a JFrame. + */ + private JFrame frame; + + /** + * The component reference, if we are drawing to a JComponent. + */ + private JComponent component; + + /** + * Get the JFrame reference. + * + * @return the frame, or null if this is drawing to a JComponent + */ + public JFrame getFrame() { + return frame; + } + + /** + * Get the JComponent reference. + * + * @return the component, or null if this is drawing to a JFrame + */ + public JComponent getComponent() { + return component; + } + + /** + * Construct using a JFrame. + * + * @param frame the JFrame to draw to + */ + public SwingComponent(final JFrame frame) { + this.frame = frame; + setupFrame(); + } + + /** + * Construct using a JComponent. + * + * @param component the JComponent to draw to + */ + public SwingComponent(final JComponent component) { + this.component = component; + setupComponent(); + } + + /** + * Setup to render to an existing JComponent. + */ + public void setupComponent() { + component.setBackground(Color.black); + + // Kill the X11 cursor + // Transparent 16 x 16 pixel cursor image. + BufferedImage cursorImg = new BufferedImage(16, 16, + BufferedImage.TYPE_INT_ARGB); + // Create a new blank cursor. + Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor( + cursorImg, new Point(0, 0), "blank cursor"); + component.setCursor(blankCursor); + + // Be capable of seeing Tab / Shift-Tab + component.setFocusTraversalKeysEnabled(false); + } + + /** + * Setup to render to an existing JFrame. + */ + public void setupFrame() { + frame.setTitle("Jexer Application"); + frame.setBackground(Color.black); + frame.pack(); + + // Kill the X11 cursor + // Transparent 16 x 16 pixel cursor image. + BufferedImage cursorImg = new BufferedImage(16, 16, + BufferedImage.TYPE_INT_ARGB); + // Create a new blank cursor. + Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor( + cursorImg, new Point(0, 0), "blank cursor"); + frame.setCursor(blankCursor); + + // Be capable of seeing Tab / Shift-Tab + frame.setFocusTraversalKeysEnabled(false); + + // Setup triple-buffering + if (tripleBuffer) { + frame.setIgnoreRepaint(true); + frame.createBufferStrategy(3); + } + } + + /** + * Set the window title. + * + * @param title the new title + */ + public void setTitle(final String title) { + if (frame != null) { + frame.setTitle(title); + } + } + + /** + * Paints this component. + * + * @param g the graphics context to use for painting + */ + public void paint(Graphics g) { + if (frame != null) { + frame.paint(g); + } else { + component.paint(g); + } + } + + /** + * Repaints this component. + */ + public void repaint() { + if (frame != null) { + frame.repaint(); + } else { + component.repaint(); + } + } + + /** + * Repaints the specified rectangle of this component. + * + * @param x the x coordinate + * @param y the y coordinate + * @param width the width + * @param height the height + */ + public void repaint(int x, int y, int width, int height) { + if (frame != null) { + frame.repaint(x, y, width, height); + } else { + component.repaint(x, y, width, height); + } + } + + /** + * If a border has been set on this component, returns the border's + * insets; otherwise calls super.getInsets. + * + * @return the value of the insets property + */ + public Insets getInsets() { + if (frame != null) { + return frame.getInsets(); + } else { + return component.getInsets(); + } + } + + /** + * Returns the current width of this component. + * + * @return the current width of this component + */ + public int getWidth() { + if (frame != null) { + return frame.getWidth(); + } else { + return component.getWidth(); + } + } + + /** + * Returns the current height of this component. + * + * @return the current height of this component + */ + public int getHeight() { + if (frame != null) { + return frame.getHeight(); + } else { + return component.getHeight(); + } + } + + /** + * Gets the font of this component. + * + * @return this component's font; if a font has not been set for this + * component, the font of its parent is returned + */ + public Font getFont() { + if (frame != null) { + return frame.getFont(); + } else { + return component.getFont(); + } + } + + /** + * Sets the font of this component. + * + * @param f the font to become this component's font; if this parameter + * is null then this component will inherit the font of its parent + */ + public void setFont(final Font f) { + if (frame != null) { + frame.setFont(f); + } else { + component.setFont(f); + } + } + + /** + * Shows or hides this Window depending on the value of parameter b. + * + * @param b if true, make visible, else make invisible + */ + public void setVisible(final boolean b) { + if (frame != null) { + frame.setVisible(b); + } else { + component.setVisible(b); + } + } + + /** + * Creates a graphics context for this component. This method will return + * null if this component is currently not displayable. + * + * @return a graphics context for this component, or null if it has none + */ + public Graphics getGraphics() { + if (frame != null) { + return frame.getGraphics(); + } else { + return component.getGraphics(); + } + } + + /** + * Releases all of the native screen resources used by this Window, its + * subcomponents, and all of its owned children. That is, the resources + * for these Components will be destroyed, any memory they consume will + * be returned to the OS, and they will be marked as undisplayable. + */ + public void dispose() { + if (frame != null) { + frame.dispose(); + } else { + component.getParent().remove(component); + } + } + + /** + * Resize the component to match the font dimensions. + * + * @param width the new width in pixels + * @param height the new height in pixels + */ + public void setDimensions(final int width, final int height) { + // Figure out the thickness of borders and use that to set the final + // size. + Insets insets = getInsets(); + + if (frame != null) { + frame.setSize(width + insets.left + insets.right, + height + insets.top + insets.bottom); + } else { + component.setSize(width + insets.left + insets.right, + height + insets.top + insets.bottom); + } + } + + /** + * Adds the specified component listener to receive component events from + * this component. If listener l is null, no exception is thrown and no + * action is performed. + * + * @param l the component listener + */ + public void addComponentListener(ComponentListener l) { + if (frame != null) { + frame.addComponentListener(l); + } else { + component.addComponentListener(l); + } + } + + /** + * Adds the specified key listener to receive key events from this + * component. If l is null, no exception is thrown and no action is + * performed. + * + * @param l the key listener. + */ + public void addKeyListener(KeyListener l) { + if (frame != null) { + frame.addKeyListener(l); + } else { + component.addKeyListener(l); + } + } + + /** + * Adds the specified mouse listener to receive mouse events from this + * component. If listener l is null, no exception is thrown and no action + * is performed. + * + * @param l the mouse listener + */ + public void addMouseListener(MouseListener l) { + if (frame != null) { + frame.addMouseListener(l); + } else { + component.addMouseListener(l); + } + } + + /** + * Adds the specified mouse motion listener to receive mouse motion + * events from this component. If listener l is null, no exception is + * thrown and no action is performed. + * + * @param l the mouse motion listener + */ + public void addMouseMotionListener(MouseMotionListener l) { + if (frame != null) { + frame.addMouseMotionListener(l); + } else { + component.addMouseMotionListener(l); + } + } + + /** + * Adds the specified mouse wheel listener to receive mouse wheel events + * from this component. Containers also receive mouse wheel events from + * sub-components. + * + * @param l the mouse wheel listener + */ + public void addMouseWheelListener(MouseWheelListener l) { + if (frame != null) { + frame.addMouseWheelListener(l); + } else { + component.addMouseWheelListener(l); + } + } + + /** + * Adds the specified window listener to receive window events from this + * window. If l is null, no exception is thrown and no action is + * performed. + * + * @param l the window listener + */ + public void addWindowListener(WindowListener l) { + if (frame != null) { + frame.addWindowListener(l); + } + } + +} diff --git a/src/jexer/session/SwingSessionInfo.java b/src/jexer/backend/SwingSessionInfo.java similarity index 74% rename from src/jexer/session/SwingSessionInfo.java rename to src/jexer/backend/SwingSessionInfo.java index dcba27d..7df751d 100644 --- a/src/jexer/session/SwingSessionInfo.java +++ b/src/jexer/backend/SwingSessionInfo.java @@ -26,32 +26,31 @@ * @author Kevin Lamonte [kevin.lamonte@gmail.com] * @version 1 */ -package jexer.session; +package jexer.backend; -import java.awt.Frame; import java.awt.Insets; /** - * SwingSessionInfo provides a session implementation with a callback into an - * Swing Frame to support queryWindowSize(). The username is blank, language - * is "en_US", with a 132x40 text window. + * SwingSessionInfo provides a session implementation with a callback into + * Swing to support queryWindowSize(). The username is blank, language is + * "en_US", with a 80x25 text window. */ public final class SwingSessionInfo implements SessionInfo { /** - * The Swing Frame. + * The Swing JFrame or JComponent. */ - private Frame frame; + private SwingComponent swing; /** * The width of a text cell in pixels. */ - private int textWidth; + private int textWidth = 10; /** * The height of a text cell in pixels. */ - private int textHeight; + private int textHeight = 10; /** * User name. @@ -127,40 +126,49 @@ public final class SwingSessionInfo implements SessionInfo { return windowHeight; } + /** + * Set the dimensions of a single text cell. + * + * @param textWidth the width of a cell in pixels + * @param textHeight the height of a cell in pixels + */ + public void setTextCellDimensions(final int textWidth, + final int textHeight) { + + this.textWidth = textWidth; + this.textHeight = textHeight; + } + /** * Public constructor. * - * @param frame the Swing Frame + * @param swing the Swing JFrame or JComponent * @param textWidth the width of a cell in pixels * @param textHeight the height of a cell in pixels - * @param windowWidth the number of text columns to start with - * @param windowHeight the number of text rows to start with */ - public SwingSessionInfo(final Frame frame, final int textWidth, - final int textHeight, final int windowWidth, final int windowHeight) { + public SwingSessionInfo(final SwingComponent swing, final int textWidth, + final int textHeight) { - this.frame = frame; - this.textWidth = textWidth; - this.textHeight = textHeight; - this.windowWidth = windowWidth; - this.windowHeight = windowHeight; + this.swing = swing; + this.textWidth = textWidth; + this.textHeight = textHeight; } /** * Re-query the text window size. */ public void queryWindowSize() { - Insets insets = frame.getInsets(); - int height = frame.getHeight() - insets.top - insets.bottom; - int width = frame.getWidth() - insets.left - insets.right; + Insets insets = swing.getInsets(); + int width = swing.getWidth() - insets.left - insets.right; + int height = swing.getHeight() - insets.top - insets.bottom; windowWidth = width / textWidth; windowHeight = height / textHeight; /* System.err.printf("queryWindowSize(): frame %d %d window %d %d\n", - frame.getWidth(), frame.getHeight(), + swing.getWidth(), swing.getHeight(), windowWidth, windowHeight); - */ + */ } diff --git a/src/jexer/backend/SwingTerminal.java b/src/jexer/backend/SwingTerminal.java new file mode 100644 index 0000000..c2fbfb4 --- /dev/null +++ b/src/jexer/backend/SwingTerminal.java @@ -0,0 +1,1886 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.backend; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Graphics; +import java.awt.Insets; +import java.awt.Rectangle; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.io.InputStream; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; + +import jexer.TKeypress; +import jexer.bits.Cell; +import jexer.bits.CellAttributes; +import jexer.event.TCommandEvent; +import jexer.event.TInputEvent; +import jexer.event.TKeypressEvent; +import jexer.event.TMouseEvent; +import jexer.event.TResizeEvent; +import static jexer.TCommand.*; +import static jexer.TKeypress.*; + +/** + * This Screen backend reads keystrokes and mouse events and draws to either + * a Java Swing JFrame (potentially triple-buffered) or a JComponent. + * + * This class is a bit of an inversion of typical GUI classes. It performs + * all of the drawing logic from SwingTerminal (which is not a Swing class), + * and uses a SwingComponent wrapper class to call the JFrame or JComponent + * methods. + */ +public final class SwingTerminal extends LogicalScreen + implements TerminalReader, + ComponentListener, KeyListener, + MouseListener, MouseMotionListener, + MouseWheelListener, WindowListener { + + /** + * The Swing component or frame to draw to. + */ + private SwingComponent swing; + + // ------------------------------------------------------------------------ + // Screen ----------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Cursor style to draw. + */ + public enum CursorStyle { + /** + * Use an underscore for the cursor. + */ + UNDERLINE, + + /** + * Use a solid block for the cursor. + */ + BLOCK, + + /** + * Use an outlined block for the cursor. + */ + OUTLINE + } + + /** + * A cache of previously-rendered glyphs for blinking text, when it is + * not visible. + */ + private HashMap glyphCacheBlink; + + /** + * A cache of previously-rendered glyphs for non-blinking, or + * blinking-and-visible, text. + */ + private HashMap glyphCache; + + // Colors to map DOS colors to AWT colors. + private static Color MYBLACK; + private static Color MYRED; + private static Color MYGREEN; + private static Color MYYELLOW; + private static Color MYBLUE; + private static Color MYMAGENTA; + private static Color MYCYAN; + private static Color MYWHITE; + private static Color MYBOLD_BLACK; + private static Color MYBOLD_RED; + private static Color MYBOLD_GREEN; + private static Color MYBOLD_YELLOW; + private static Color MYBOLD_BLUE; + private static Color MYBOLD_MAGENTA; + private static Color MYBOLD_CYAN; + private static Color MYBOLD_WHITE; + + /** + * When true, all the MYBLACK, MYRED, etc. colors are set. + */ + private static boolean dosColors = false; + + /** + * Setup Swing colors to match DOS color palette. + */ + private static void setDOSColors() { + if (dosColors) { + return; + } + MYBLACK = new Color(0x00, 0x00, 0x00); + MYRED = new Color(0xa8, 0x00, 0x00); + MYGREEN = new Color(0x00, 0xa8, 0x00); + MYYELLOW = new Color(0xa8, 0x54, 0x00); + MYBLUE = new Color(0x00, 0x00, 0xa8); + MYMAGENTA = new Color(0xa8, 0x00, 0xa8); + MYCYAN = new Color(0x00, 0xa8, 0xa8); + MYWHITE = new Color(0xa8, 0xa8, 0xa8); + MYBOLD_BLACK = new Color(0x54, 0x54, 0x54); + MYBOLD_RED = new Color(0xfc, 0x54, 0x54); + MYBOLD_GREEN = new Color(0x54, 0xfc, 0x54); + MYBOLD_YELLOW = new Color(0xfc, 0xfc, 0x54); + MYBOLD_BLUE = new Color(0x54, 0x54, 0xfc); + MYBOLD_MAGENTA = new Color(0xfc, 0x54, 0xfc); + MYBOLD_CYAN = new Color(0x54, 0xfc, 0xfc); + MYBOLD_WHITE = new Color(0xfc, 0xfc, 0xfc); + + dosColors = true; + } + + /** + * The terminus font resource filename. + */ + private static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf"; + + /** + * If true, we were successful getting Terminus. + */ + private boolean gotTerminus = false; + + /** + * If true, we were successful at getting the font dimensions. + */ + private boolean gotFontDimensions = false; + + /** + * The currently selected font. + */ + private Font font = null; + + /** + * The currently selected font size in points. + */ + private int fontSize = 16; + + /** + * Width of a character cell in pixels. + */ + private int textWidth = 1; + + /** + * Height of a character cell in pixels. + */ + private int textHeight = 1; + + /** + * Descent of a character cell in pixels. + */ + private int maxDescent = 0; + + /** + * System-dependent Y adjustment for text in the character cell. + */ + private int textAdjustY = 0; + + /** + * System-dependent X adjustment for text in the character cell. + */ + private int textAdjustX = 0; + + /** + * Top pixel absolute location. + */ + private int top = 30; + + /** + * Left pixel absolute location. + */ + private int left = 30; + + /** + * The cursor style to draw. + */ + private CursorStyle cursorStyle = CursorStyle.UNDERLINE; + + /** + * The number of millis to wait before switching the blink from + * visible to invisible. + */ + private long blinkMillis = 500; + + /** + * If true, the cursor should be visible right now based on the blink + * time. + */ + private boolean cursorBlinkVisible = true; + + /** + * The time that the blink last flipped from visible to invisible or + * from invisible to visible. + */ + private long lastBlinkTime = 0; + + /** + * Get the font size in points. + * + * @return font size in points + */ + public int getFontSize() { + return fontSize; + } + + /** + * Set the font size in points. + * + * @param fontSize font size in points + */ + public void setFontSize(final int fontSize) { + this.fontSize = fontSize; + Font newFont = font.deriveFont((float) fontSize); + setFont(newFont); + } + + /** + * Set to a new font, and resize the screen to match its dimensions. + * + * @param font the new font + */ + public void setFont(final Font font) { + this.font = font; + getFontDimensions(); + swing.setFont(font); + glyphCacheBlink = new HashMap(); + glyphCache = new HashMap(); + resizeToScreen(); + } + + /** + * Set the font to Terminus, the best all-around font for both CP437 and + * ISO8859-1. + */ + public void getDefaultFont() { + try { + ClassLoader loader = Thread.currentThread(). + getContextClassLoader(); + InputStream in = loader.getResourceAsStream(FONTFILE); + Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in); + Font terminus = terminusRoot.deriveFont(Font.PLAIN, fontSize); + gotTerminus = true; + font = terminus; + } catch (Exception e) { + e.printStackTrace(); + font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize); + } + + setFont(font); + } + + /** + * Convert a CellAttributes foreground color to an Swing Color. + * + * @param attr the text attributes + * @return the Swing Color + */ + private Color attrToForegroundColor(final CellAttributes attr) { + if (attr.isBold()) { + if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) { + return MYBOLD_BLACK; + } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) { + return MYBOLD_RED; + } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) { + return MYBOLD_BLUE; + } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) { + return MYBOLD_GREEN; + } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) { + return MYBOLD_YELLOW; + } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) { + return MYBOLD_CYAN; + } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) { + return MYBOLD_MAGENTA; + } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) { + return MYBOLD_WHITE; + } + } else { + if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) { + return MYBLACK; + } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) { + return MYRED; + } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) { + return MYBLUE; + } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) { + return MYGREEN; + } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) { + return MYYELLOW; + } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) { + return MYCYAN; + } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) { + return MYMAGENTA; + } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) { + return MYWHITE; + } + } + throw new IllegalArgumentException("Invalid color: " + + attr.getForeColor().getValue()); + } + + /** + * Convert a CellAttributes background color to an Swing Color. + * + * @param attr the text attributes + * @return the Swing Color + */ + private Color attrToBackgroundColor(final CellAttributes attr) { + if (attr.getBackColor().equals(jexer.bits.Color.BLACK)) { + return MYBLACK; + } else if (attr.getBackColor().equals(jexer.bits.Color.RED)) { + return MYRED; + } else if (attr.getBackColor().equals(jexer.bits.Color.BLUE)) { + return MYBLUE; + } else if (attr.getBackColor().equals(jexer.bits.Color.GREEN)) { + return MYGREEN; + } else if (attr.getBackColor().equals(jexer.bits.Color.YELLOW)) { + return MYYELLOW; + } else if (attr.getBackColor().equals(jexer.bits.Color.CYAN)) { + return MYCYAN; + } else if (attr.getBackColor().equals(jexer.bits.Color.MAGENTA)) { + return MYMAGENTA; + } else if (attr.getBackColor().equals(jexer.bits.Color.WHITE)) { + return MYWHITE; + } + throw new IllegalArgumentException("Invalid color: " + + attr.getBackColor().getValue()); + } + + /** + * Figure out what textAdjustX and textAdjustY should be, based on the + * location of a vertical bar (to find textAdjustY) and a horizontal bar + * (to find textAdjustX). + * + * @return true if textAdjustX and textAdjustY were guessed at correctly + */ + private boolean getFontAdjustments() { + BufferedImage image = null; + + // What SHOULD happen is that the topmost/leftmost white pixel is at + // position (gr2x, gr2y). But it might also be off by a pixel in + // either direction. + + Graphics2D gr2 = null; + int gr2x = 3; + int gr2y = 3; + image = new BufferedImage(textWidth * 2, textHeight * 2, + BufferedImage.TYPE_INT_ARGB); + + gr2 = image.createGraphics(); + gr2.setFont(swing.getFont()); + gr2.setColor(java.awt.Color.BLACK); + gr2.fillRect(0, 0, textWidth * 2, textHeight * 2); + gr2.setColor(java.awt.Color.WHITE); + char [] chars = new char[1]; + chars[0] = jexer.bits.GraphicsChars.VERTICAL_BAR; + gr2.drawChars(chars, 0, 1, gr2x, gr2y + textHeight - maxDescent); + gr2.dispose(); + + for (int x = 0; x < textWidth; x++) { + for (int y = 0; y < textHeight; y++) { + + /* + System.err.println("X: " + x + " Y: " + y + " " + + image.getRGB(x, y)); + */ + + if ((image.getRGB(x, y) & 0xFFFFFF) != 0) { + textAdjustY = (gr2y - y); + + // System.err.println("textAdjustY: " + textAdjustY); + x = textWidth; + break; + } + } + } + + gr2 = image.createGraphics(); + gr2.setFont(swing.getFont()); + gr2.setColor(java.awt.Color.BLACK); + gr2.fillRect(0, 0, textWidth * 2, textHeight * 2); + gr2.setColor(java.awt.Color.WHITE); + chars[0] = jexer.bits.GraphicsChars.SINGLE_BAR; + gr2.drawChars(chars, 0, 1, gr2x, gr2y + textHeight - maxDescent); + gr2.dispose(); + + for (int x = 0; x < textWidth; x++) { + for (int y = 0; y < textHeight; y++) { + + /* + System.err.println("X: " + x + " Y: " + y + " " + + image.getRGB(x, y)); + */ + + if ((image.getRGB(x, y) & 0xFFFFFF) != 0) { + textAdjustX = (gr2x - x); + + // System.err.println("textAdjustX: " + textAdjustX); + return true; + } + } + } + + // Something weird happened, don't rely on this function. + // System.err.println("getFontAdjustments: false"); + return false; + } + + /** + * Figure out my font dimensions. This code path works OK for the JFrame + * case, and can be called immediately after JFrame creation. + */ + private void getFontDimensions() { + swing.setFont(font); + Graphics gr = swing.getGraphics(); + if (gr == null) { + return; + } + getFontDimensions(gr); + } + + /** + * Figure out my font dimensions. This code path is needed to lazy-load + * the information inside paint(). + * + * @param gr Graphics object to use + */ + private void getFontDimensions(final Graphics gr) { + swing.setFont(font); + FontMetrics fm = gr.getFontMetrics(); + maxDescent = fm.getMaxDescent(); + Rectangle2D bounds = fm.getMaxCharBounds(gr); + int leading = fm.getLeading(); + textWidth = (int)Math.round(bounds.getWidth()); + // textHeight = (int)Math.round(bounds.getHeight()) - maxDescent; + + // This produces the same number, but works better for ugly + // monospace. + textHeight = fm.getMaxAscent() + maxDescent - leading; + + // TODO: is this still necessary? + if (gotTerminus == true) { + textHeight++; + } + + if (getFontAdjustments() == false) { + // We were unable to programmatically determine textAdjustX and + // textAdjustY, so try some guesses based on VM vendor. + String runtime = System.getProperty("java.runtime.name"); + if ((runtime != null) && (runtime.contains("Java(TM)"))) { + textAdjustY = -1; + textAdjustX = 0; + } + } + + if (sessionInfo != null) { + sessionInfo.setTextCellDimensions(textWidth, textHeight); + } + gotFontDimensions = true; + } + + /** + * Resize to font dimensions. + */ + public void resizeToScreen() { + swing.setDimensions(textWidth * width, textHeight * height); + } + + /** + * Draw one glyph to the screen. + * + * @param gr the Swing Graphics context + * @param cell the Cell to draw + * @param xPixel the x-coordinate to render to. 0 means the + * left-most pixel column. + * @param yPixel the y-coordinate to render to. 0 means the top-most + * pixel row. + */ + private void drawGlyph(final Graphics gr, final Cell cell, + final int xPixel, final int yPixel) { + + /* + System.err.println("drawGlyph(): " + xPixel + " " + yPixel + + " " + cell); + */ + + BufferedImage image = null; + if (cell.isBlink() && !cursorBlinkVisible) { + image = glyphCacheBlink.get(cell); + } else { + image = glyphCache.get(cell); + } + if (image != null) { + if (swing.getFrame() != null) { + gr.drawImage(image, xPixel, yPixel, swing.getFrame()); + } else { + gr.drawImage(image, xPixel, yPixel, swing.getComponent()); + } + return; + } + + // Generate glyph and draw it. + Graphics2D gr2 = null; + int gr2x = xPixel; + int gr2y = yPixel; + if ((SwingComponent.tripleBuffer) && (swing.getFrame() != null)) { + image = new BufferedImage(textWidth, textHeight, + BufferedImage.TYPE_INT_ARGB); + gr2 = image.createGraphics(); + gr2.setFont(swing.getFont()); + gr2x = 0; + gr2y = 0; + } else { + gr2 = (Graphics2D) gr; + } + + Cell cellColor = new Cell(); + cellColor.setTo(cell); + + // Check for reverse + if (cell.isReverse()) { + cellColor.setForeColor(cell.getBackColor()); + cellColor.setBackColor(cell.getForeColor()); + } + + // Draw the background rectangle, then the foreground character. + gr2.setColor(attrToBackgroundColor(cellColor)); + gr2.fillRect(gr2x, gr2y, textWidth, textHeight); + + // Handle blink and underline + if (!cell.isBlink() + || (cell.isBlink() && cursorBlinkVisible) + ) { + gr2.setColor(attrToForegroundColor(cellColor)); + char [] chars = new char[1]; + chars[0] = cell.getChar(); + gr2.drawChars(chars, 0, 1, gr2x + textAdjustX, + gr2y + textHeight - maxDescent + textAdjustY); + + if (cell.isUnderline()) { + gr2.fillRect(gr2x, gr2y + textHeight - 2, textWidth, 2); + } + } + + if ((SwingComponent.tripleBuffer) && (swing.getFrame() != null)) { + gr2.dispose(); + + // We need a new key that will not be mutated by + // invertCell(). + Cell key = new Cell(); + key.setTo(cell); + if (cell.isBlink() && !cursorBlinkVisible) { + glyphCacheBlink.put(key, image); + } else { + glyphCache.put(key, image); + } + + if (swing.getFrame() != null) { + gr.drawImage(image, xPixel, yPixel, swing.getFrame()); + } else { + gr.drawImage(image, xPixel, yPixel, swing.getComponent()); + } + } + + } + + /** + * Check if the cursor is visible, and if so draw it. + * + * @param gr the Swing Graphics context + */ + private void drawCursor(final Graphics gr) { + + if (cursorVisible + && (cursorY <= height - 1) + && (cursorX <= width - 1) + && cursorBlinkVisible + ) { + int xPixel = cursorX * textWidth + left; + int yPixel = cursorY * textHeight + top; + Cell lCell = logical[cursorX][cursorY]; + gr.setColor(attrToForegroundColor(lCell)); + switch (cursorStyle) { + default: + // Fall through... + case UNDERLINE: + gr.fillRect(xPixel, yPixel + textHeight - 2, textWidth, 2); + break; + case BLOCK: + gr.fillRect(xPixel, yPixel, textWidth, textHeight); + break; + case OUTLINE: + gr.drawRect(xPixel, yPixel, textWidth - 1, textHeight - 1); + break; + } + } + } + + /** + * Paint redraws the whole screen. + * + * @param gr the Swing Graphics context + */ + public void paint(final Graphics gr) { + + if (gotFontDimensions == false) { + // Lazy-load the text width/height + // System.err.println("calling getFontDimensions..."); + getFontDimensions(gr); + /* + System.err.println("textWidth " + textWidth + + " textHeight " + textHeight); + System.err.println("FONT: " + swing.getFont() + " font " + font); + */ + // resizeToScreen(); + } + + // See if it is time to flip the blink time. + long nowTime = (new Date()).getTime(); + if (nowTime > blinkMillis + lastBlinkTime) { + lastBlinkTime = nowTime; + cursorBlinkVisible = !cursorBlinkVisible; + } + + int xCellMin = 0; + int xCellMax = width; + int yCellMin = 0; + int yCellMax = height; + + Rectangle bounds = gr.getClipBounds(); + if (bounds != null) { + // Only update what is in the bounds + xCellMin = textColumn(bounds.x); + xCellMax = textColumn(bounds.x + bounds.width); + if (xCellMax > width) { + xCellMax = width; + } + if (xCellMin >= xCellMax) { + xCellMin = xCellMax - 2; + } + if (xCellMin < 0) { + xCellMin = 0; + } + yCellMin = textRow(bounds.y); + yCellMax = textRow(bounds.y + bounds.height); + if (yCellMax > height) { + yCellMax = height; + } + if (yCellMin >= yCellMax) { + yCellMin = yCellMax - 2; + } + if (yCellMin < 0) { + yCellMin = 0; + } + } else { + // We need a total repaint + reallyCleared = true; + } + + // Prevent updates to the screen's data from the TApplication + // threads. + synchronized (this) { + + /* + System.err.printf("bounds %s X %d %d Y %d %d\n", + bounds, xCellMin, xCellMax, yCellMin, yCellMax); + */ + + for (int y = yCellMin; y < yCellMax; y++) { + for (int x = xCellMin; x < xCellMax; x++) { + + int xPixel = x * textWidth + left; + int yPixel = y * textHeight + top; + + Cell lCell = logical[x][y]; + Cell pCell = physical[x][y]; + + if (!lCell.equals(pCell) + || lCell.isBlink() + || reallyCleared + || (swing.getFrame() == null)) { + + drawGlyph(gr, lCell, xPixel, yPixel); + + // Physical is always updated + physical[x][y].setTo(lCell); + } + } + } + drawCursor(gr); + + dirty = false; + reallyCleared = false; + } // synchronized (this) + } + + /** + * Restore terminal to normal state. + */ + public void shutdown() { + swing.dispose(); + } + + /** + * Push the logical screen to the physical device. + */ + @Override + public void flushPhysical() { + + /* + System.err.printf("flushPhysical(): reallyCleared %s dirty %s\n", + reallyCleared, dirty); + */ + + // If reallyCleared is set, we have to draw everything. + if ((swing.getFrame() != null) + && (swing.getBufferStrategy() != null) + && (reallyCleared == true) + ) { + // Triple-buffering: we have to redraw everything on this thread. + Graphics gr = swing.getBufferStrategy().getDrawGraphics(); + swing.paint(gr); + gr.dispose(); + swing.getBufferStrategy().show(); + // sync() doesn't seem to help the tearing for me. + // Toolkit.getDefaultToolkit().sync(); + return; + } else if (((swing.getFrame() != null) + && (swing.getBufferStrategy() == null)) + || (reallyCleared == true) + ) { + // Repaint everything on the Swing thread. + // System.err.println("REPAINT ALL"); + swing.repaint(); + return; + } + + // Do nothing if nothing happened. + if (!dirty) { + return; + } + + if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) { + // See if it is time to flip the blink time. + long nowTime = (new Date()).getTime(); + if (nowTime > blinkMillis + lastBlinkTime) { + lastBlinkTime = nowTime; + cursorBlinkVisible = !cursorBlinkVisible; + } + + Graphics gr = swing.getBufferStrategy().getDrawGraphics(); + + synchronized (this) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + Cell lCell = logical[x][y]; + Cell pCell = physical[x][y]; + + int xPixel = x * textWidth + left; + int yPixel = y * textHeight + top; + + if (!lCell.equals(pCell) + || ((x == cursorX) + && (y == cursorY) + && cursorVisible) + || (lCell.isBlink()) + ) { + drawGlyph(gr, lCell, xPixel, yPixel); + physical[x][y].setTo(lCell); + } + } + } + drawCursor(gr); + } // synchronized (this) + + gr.dispose(); + swing.getBufferStrategy().show(); + // sync() doesn't seem to help the tearing for me. + // Toolkit.getDefaultToolkit().sync(); + return; + } + + // Swing thread version: request a repaint, but limit it to the area + // that has changed. + + // Find the minimum-size damaged region. + int xMin = swing.getWidth(); + int xMax = 0; + int yMin = swing.getHeight(); + int yMax = 0; + + synchronized (this) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + Cell lCell = logical[x][y]; + Cell pCell = physical[x][y]; + + int xPixel = x * textWidth + left; + int yPixel = y * textHeight + top; + + if (!lCell.equals(pCell) + || ((x == cursorX) + && (y == cursorY) + && cursorVisible) + || lCell.isBlink() + ) { + if (xPixel < xMin) { + xMin = xPixel; + } + if (xPixel + textWidth > xMax) { + xMax = xPixel + textWidth; + } + if (yPixel < yMin) { + yMin = yPixel; + } + if (yPixel + textHeight > yMax) { + yMax = yPixel + textHeight; + } + } + } + } + } + if (xMin + textWidth >= xMax) { + xMax += textWidth; + } + if (yMin + textHeight >= yMax) { + yMax += textHeight; + } + + // Repaint the desired area + /* + System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax, + yMin, yMax); + */ + + if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) { + // This path should never be taken, but is left here for + // completeness. + Graphics gr = swing.getBufferStrategy().getDrawGraphics(); + Rectangle bounds = new Rectangle(xMin, yMin, xMax - xMin, + yMax - yMin); + gr.setClip(bounds); + swing.paint(gr); + gr.dispose(); + swing.getBufferStrategy().show(); + // sync() doesn't seem to help the tearing for me. + // Toolkit.getDefaultToolkit().sync(); + } else { + // Repaint on the Swing thread. + swing.repaint(xMin, yMin, xMax - xMin, yMax - yMin); + } + } + + /** + * Put the cursor at (x,y). + * + * @param visible if true, the cursor should be visible + * @param x column coordinate to put the cursor on + * @param y row coordinate to put the cursor on + */ + @Override + public void putCursor(final boolean visible, final int x, final int y) { + + if ((visible == cursorVisible) && ((x == cursorX) && (y == cursorY))) { + // See if it is time to flip the blink time. + long nowTime = (new Date()).getTime(); + if (nowTime < blinkMillis + lastBlinkTime) { + // Nothing has changed, so don't do anything. + return; + } + } + + if (cursorVisible + && (cursorY <= height - 1) + && (cursorX <= width - 1) + ) { + // Make the current cursor position dirty + if (physical[cursorX][cursorY].getChar() == 'Q') { + physical[cursorX][cursorY].setChar('X'); + } else { + physical[cursorX][cursorY].setChar('Q'); + } + } + + super.putCursor(visible, x, y); + } + + /** + * Convert pixel column position to text cell column position. + * + * @param x pixel column position + * @return text cell column position + */ + public int textColumn(final int x) { + return ((x - left) / textWidth); + } + + /** + * Convert pixel row position to text cell row position. + * + * @param y pixel row position + * @return text cell row position + */ + public int textRow(final int y) { + return ((y - top) / textHeight); + } + + /** + * Set the window title. + * + * @param title the new title + */ + public void setTitle(final String title) { + swing.setTitle(title); + } + + // ------------------------------------------------------------------------ + // TerminalReader --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The session information. + */ + private SwingSessionInfo sessionInfo; + + /** + * Getter for sessionInfo. + * + * @return the SessionInfo + */ + public SessionInfo getSessionInfo() { + return sessionInfo; + } + + /** + * The listening object that run() wakes up on new input. + */ + private Object listener; + + /** + * Set listener to a different Object. + * + * @param listener the new listening object that run() wakes up on new + * input + */ + public void setListener(final Object listener) { + this.listener = listener; + } + + /** + * The event queue, filled up by a thread reading on input. + */ + private List eventQueue; + + /** + * The last reported mouse X position. + */ + private int oldMouseX = -1; + + /** + * The last reported mouse Y position. + */ + private int oldMouseY = -1; + + /** + * true if mouse1 was down. Used to report mouse1 on the release event. + */ + private boolean mouse1 = false; + + /** + * true if mouse2 was down. Used to report mouse2 on the release event. + */ + private boolean mouse2 = false; + + /** + * true if mouse3 was down. Used to report mouse3 on the release event. + */ + private boolean mouse3 = false; + + /** + * Public constructor creates a new JFrame to render to. + * + * @param windowWidth the number of text columns to start with + * @param windowHeight the number of text rows to start with + * @param fontSize the size in points. Good values to pick are: 16, 20, + * 22, and 24. + * @param listener the object this backend needs to wake up when new + * input comes in + */ + public SwingTerminal(final int windowWidth, final int windowHeight, + final int fontSize, final Object listener) { + + this.fontSize = fontSize; + + setDOSColors(); + + // Figure out my cursor style. + String cursorStyleString = System.getProperty( + "jexer.Swing.cursorStyle", "underline").toLowerCase(); + if (cursorStyleString.equals("underline")) { + cursorStyle = CursorStyle.UNDERLINE; + } else if (cursorStyleString.equals("outline")) { + cursorStyle = CursorStyle.OUTLINE; + } else if (cursorStyleString.equals("block")) { + cursorStyle = CursorStyle.BLOCK; + } + + // Pull the system property for triple buffering. + if (System.getProperty("jexer.Swing.tripleBuffer") != null) { + if (System.getProperty("jexer.Swing.tripleBuffer"). + equals("false")) { + + SwingComponent.tripleBuffer = false; + } + } + + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + + JFrame frame = new JFrame() { + + /** + * Serializable version. + */ + private static final long serialVersionUID = 1; + + /** + * The code that performs the actual drawing. + */ + public SwingTerminal screen = null; + + /* + * Anonymous class initializer saves the screen + * reference, so that paint() and the like call out + * to SwingTerminal. + */ + { + this.screen = SwingTerminal.this; + } + + /** + * Update redraws the whole screen. + * + * @param gr the Swing Graphics context + */ + @Override + public void update(final Graphics gr) { + // The default update clears the area. Don't do + // that, instead just paint it directly. + paint(gr); + } + + /** + * Paint redraws the whole screen. + * + * @param gr the Swing Graphics context + */ + @Override + public void paint(final Graphics gr) { + if (screen != null) { + screen.paint(gr); + } + } + }; + + // Get the Swing component + SwingTerminal.this.swing = new SwingComponent(frame); + + // Hang onto top and left for drawing. + Insets insets = SwingTerminal.this.swing.getInsets(); + SwingTerminal.this.left = insets.left; + SwingTerminal.this.top = insets.top; + + // Load the font so that we can set sessionInfo. + getDefaultFont(); + + // Get the default cols x rows and set component size + // accordingly. + SwingTerminal.this.sessionInfo = + new SwingSessionInfo(SwingTerminal.this.swing, + SwingTerminal.this.textWidth, + SwingTerminal.this.textHeight); + + SwingTerminal.this.setDimensions(sessionInfo.getWindowWidth(), + sessionInfo.getWindowHeight()); + + SwingTerminal.this.resizeToScreen(); + SwingTerminal.this.swing.setVisible(true); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + + this.listener = listener; + mouse1 = false; + mouse2 = false; + mouse3 = false; + eventQueue = new LinkedList(); + + // Add listeners to Swing. + swing.addKeyListener(this); + swing.addWindowListener(this); + swing.addComponentListener(this); + swing.addMouseListener(this); + swing.addMouseMotionListener(this); + swing.addMouseWheelListener(this); + } + + /** + * Public constructor creates a new JFrame to render to. + * + * @param component the Swing component to render to + * @param windowWidth the number of text columns to start with + * @param windowHeight the number of text rows to start with + * @param fontSize the size in points. Good values to pick are: 16, 20, + * 22, and 24. + * @param listener the object this backend needs to wake up when new + * input comes in + */ + public SwingTerminal(final JComponent component, final int windowWidth, + final int windowHeight, final int fontSize, final Object listener) { + + this.fontSize = fontSize; + + setDOSColors(); + + // Figure out my cursor style. + String cursorStyleString = System.getProperty( + "jexer.Swing.cursorStyle", "underline").toLowerCase(); + if (cursorStyleString.equals("underline")) { + cursorStyle = CursorStyle.UNDERLINE; + } else if (cursorStyleString.equals("outline")) { + cursorStyle = CursorStyle.OUTLINE; + } else if (cursorStyleString.equals("block")) { + cursorStyle = CursorStyle.BLOCK; + } + + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + + JComponent newComponent = new JComponent() { + + /** + * Serializable version. + */ + private static final long serialVersionUID = 1; + + /** + * The code that performs the actual drawing. + */ + public SwingTerminal screen = null; + + /* + * Anonymous class initializer saves the screen + * reference, so that paint() and the like call out + * to SwingTerminal. + */ + { + this.screen = SwingTerminal.this; + } + + /** + * Update redraws the whole screen. + * + * @param gr the Swing Graphics context + */ + @Override + public void update(final Graphics gr) { + // The default update clears the area. Don't do + // that, instead just paint it directly. + paint(gr); + } + + /** + * Paint redraws the whole screen. + * + * @param gr the Swing Graphics context + */ + @Override + public void paint(final Graphics gr) { + if (screen != null) { + screen.paint(gr); + } + } + }; + component.setLayout(new BorderLayout()); + component.add(newComponent); + + // Get the Swing component + SwingTerminal.this.swing = new SwingComponent(component); + + // Hang onto top and left for drawing. + Insets insets = SwingTerminal.this.swing.getInsets(); + SwingTerminal.this.left = insets.left; + SwingTerminal.this.top = insets.top; + + // Load the font so that we can set sessionInfo. + getDefaultFont(); + + // Get the default cols x rows and set component size + // accordingly. + SwingTerminal.this.sessionInfo = + new SwingSessionInfo(SwingTerminal.this.swing, + SwingTerminal.this.textWidth, + SwingTerminal.this.textHeight); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + + this.listener = listener; + mouse1 = false; + mouse2 = false; + mouse3 = false; + eventQueue = new LinkedList(); + + // Add listeners to Swing. + swing.addKeyListener(this); + swing.addWindowListener(this); + swing.addComponentListener(this); + swing.addMouseListener(this); + swing.addMouseMotionListener(this); + swing.addMouseWheelListener(this); + } + + /** + * Check if there are events in the queue. + * + * @return if true, getEvents() has something to return to the backend + */ + public boolean hasEvents() { + synchronized (eventQueue) { + return (eventQueue.size() > 0); + } + } + + /** + * Return any events in the IO queue. + * + * @param queue list to append new events to + */ + public void getEvents(final List queue) { + synchronized (eventQueue) { + if (eventQueue.size() > 0) { + synchronized (queue) { + queue.addAll(eventQueue); + } + eventQueue.clear(); + } + } + } + + /** + * Restore terminal to normal state. + */ + public void closeTerminal() { + shutdown(); + } + + /** + * Pass Swing keystrokes into the event queue. + * + * @param key keystroke received + */ + public void keyReleased(final KeyEvent key) { + // Ignore release events + } + + /** + * Pass Swing keystrokes into the event queue. + * + * @param key keystroke received + */ + public void keyTyped(final KeyEvent key) { + // Ignore typed events + } + + /** + * Pass Swing keystrokes into the event queue. + * + * @param key keystroke received + */ + public void keyPressed(final KeyEvent key) { + boolean alt = false; + boolean shift = false; + boolean ctrl = false; + char ch = ' '; + boolean isKey = false; + if (key.isActionKey()) { + isKey = true; + } else { + ch = key.getKeyChar(); + } + alt = key.isAltDown(); + ctrl = key.isControlDown(); + shift = key.isShiftDown(); + + /* + System.err.printf("Swing Key: %s\n", key); + System.err.printf(" isKey: %s\n", isKey); + System.err.printf(" alt: %s\n", alt); + System.err.printf(" ctrl: %s\n", ctrl); + System.err.printf(" shift: %s\n", shift); + System.err.printf(" ch: %s\n", ch); + */ + + // Special case: not return the bare modifier presses + switch (key.getKeyCode()) { + case KeyEvent.VK_ALT: + return; + case KeyEvent.VK_ALT_GRAPH: + return; + case KeyEvent.VK_CONTROL: + return; + case KeyEvent.VK_SHIFT: + return; + case KeyEvent.VK_META: + return; + default: + break; + } + + TKeypress keypress = null; + if (isKey) { + switch (key.getKeyCode()) { + case KeyEvent.VK_F1: + keypress = new TKeypress(true, TKeypress.F1, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_F2: + keypress = new TKeypress(true, TKeypress.F2, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_F3: + keypress = new TKeypress(true, TKeypress.F3, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_F4: + keypress = new TKeypress(true, TKeypress.F4, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_F5: + keypress = new TKeypress(true, TKeypress.F5, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_F6: + keypress = new TKeypress(true, TKeypress.F6, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_F7: + keypress = new TKeypress(true, TKeypress.F7, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_F8: + keypress = new TKeypress(true, TKeypress.F8, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_F9: + keypress = new TKeypress(true, TKeypress.F9, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_F10: + keypress = new TKeypress(true, TKeypress.F10, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_F11: + keypress = new TKeypress(true, TKeypress.F11, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_F12: + keypress = new TKeypress(true, TKeypress.F12, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_HOME: + keypress = new TKeypress(true, TKeypress.HOME, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_END: + keypress = new TKeypress(true, TKeypress.END, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_PAGE_UP: + keypress = new TKeypress(true, TKeypress.PGUP, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_PAGE_DOWN: + keypress = new TKeypress(true, TKeypress.PGDN, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_INSERT: + keypress = new TKeypress(true, TKeypress.INS, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_DELETE: + keypress = new TKeypress(true, TKeypress.DEL, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_RIGHT: + keypress = new TKeypress(true, TKeypress.RIGHT, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_LEFT: + keypress = new TKeypress(true, TKeypress.LEFT, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_UP: + keypress = new TKeypress(true, TKeypress.UP, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_DOWN: + keypress = new TKeypress(true, TKeypress.DOWN, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_TAB: + // Special case: distinguish TAB vs BTAB + if (shift) { + keypress = kbShiftTab; + } else { + keypress = kbTab; + } + break; + case KeyEvent.VK_ENTER: + keypress = new TKeypress(true, TKeypress.ENTER, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_ESCAPE: + keypress = new TKeypress(true, TKeypress.ESC, ' ', + alt, ctrl, shift); + break; + case KeyEvent.VK_BACK_SPACE: + // Special case: return it as kbBackspace (Ctrl-H) + keypress = new TKeypress(false, 0, 'H', false, true, false); + break; + default: + // Unsupported, ignore + return; + } + } + + if (keypress == null) { + switch (ch) { + case 0x08: + keypress = kbBackspace; + break; + case 0x0A: + keypress = kbEnter; + break; + case 0x1B: + keypress = kbEsc; + break; + case 0x0D: + keypress = kbEnter; + break; + case 0x09: + if (shift) { + keypress = kbShiftTab; + } else { + keypress = kbTab; + } + break; + case 0x7F: + keypress = kbDel; + break; + default: + if (!alt && ctrl && !shift) { + ch = KeyEvent.getKeyText(key.getKeyCode()).charAt(0); + } + // Not a special key, put it together + keypress = new TKeypress(false, 0, ch, alt, ctrl, shift); + } + } + + // Save it and we are done. + synchronized (eventQueue) { + eventQueue.add(new TKeypressEvent(keypress)); + } + synchronized (listener) { + listener.notifyAll(); + } + } + + /** + * Pass window events into the event queue. + * + * @param event window event received + */ + public void windowActivated(final WindowEvent event) { + // Force a total repaint + synchronized (this) { + clearPhysical(); + } + } + + /** + * Pass window events into the event queue. + * + * @param event window event received + */ + public void windowClosed(final WindowEvent event) { + // Ignore + } + + /** + * Pass window events into the event queue. + * + * @param event window event received + */ + public void windowClosing(final WindowEvent event) { + // Drop a cmAbort and walk away + synchronized (eventQueue) { + eventQueue.add(new TCommandEvent(cmAbort)); + } + synchronized (listener) { + listener.notifyAll(); + } + } + + /** + * Pass window events into the event queue. + * + * @param event window event received + */ + public void windowDeactivated(final WindowEvent event) { + // Ignore + } + + /** + * Pass window events into the event queue. + * + * @param event window event received + */ + public void windowDeiconified(final WindowEvent event) { + // Ignore + } + + /** + * Pass window events into the event queue. + * + * @param event window event received + */ + public void windowIconified(final WindowEvent event) { + // Ignore + } + + /** + * Pass window events into the event queue. + * + * @param event window event received + */ + public void windowOpened(final WindowEvent event) { + // Ignore + } + + /** + * Pass component events into the event queue. + * + * @param event component event received + */ + public void componentHidden(final ComponentEvent event) { + // Ignore + } + + /** + * Pass component events into the event queue. + * + * @param event component event received + */ + public void componentShown(final ComponentEvent event) { + // Ignore + } + + /** + * Pass component events into the event queue. + * + * @param event component event received + */ + public void componentMoved(final ComponentEvent event) { + // Ignore + } + + /** + * Pass component events into the event queue. + * + * @param event component event received + */ + public void componentResized(final ComponentEvent event) { + if (gotFontDimensions == false) { + // We are still waiting to get font information. Don't pass a + // resize event up. + // System.err.println("size " + swing.getComponent().getSize()); + return; + } + + // Drop a new TResizeEvent into the queue + sessionInfo.queryWindowSize(); + synchronized (eventQueue) { + TResizeEvent windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, + sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); + eventQueue.add(windowResize); + } + synchronized (listener) { + listener.notifyAll(); + } + } + + /** + * Pass mouse events into the event queue. + * + * @param mouse mouse event received + */ + public void mouseDragged(final MouseEvent mouse) { + int modifiers = mouse.getModifiersEx(); + boolean eventMouse1 = false; + boolean eventMouse2 = false; + boolean eventMouse3 = false; + if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { + eventMouse1 = true; + } + if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) { + eventMouse2 = true; + } + if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { + eventMouse3 = true; + } + mouse1 = eventMouse1; + mouse2 = eventMouse2; + mouse3 = eventMouse3; + int x = textColumn(mouse.getX()); + int y = textRow(mouse.getY()); + + TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION, + x, y, x, y, mouse1, mouse2, mouse3, false, false); + + synchronized (eventQueue) { + eventQueue.add(mouseEvent); + } + synchronized (listener) { + listener.notifyAll(); + } + } + + /** + * Pass mouse events into the event queue. + * + * @param mouse mouse event received + */ + public void mouseMoved(final MouseEvent mouse) { + int x = textColumn(mouse.getX()); + int y = textRow(mouse.getY()); + if ((x == oldMouseX) && (y == oldMouseY)) { + // Bail out, we've moved some pixels but not a whole text cell. + return; + } + oldMouseX = x; + oldMouseY = y; + + TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION, + x, y, x, y, mouse1, mouse2, mouse3, false, false); + + synchronized (eventQueue) { + eventQueue.add(mouseEvent); + } + synchronized (listener) { + listener.notifyAll(); + } + } + + /** + * Pass mouse events into the event queue. + * + * @param mouse mouse event received + */ + public void mouseClicked(final MouseEvent mouse) { + // Ignore + } + + /** + * Pass mouse events into the event queue. + * + * @param mouse mouse event received + */ + public void mouseEntered(final MouseEvent mouse) { + // Ignore + } + + /** + * Pass mouse events into the event queue. + * + * @param mouse mouse event received + */ + public void mouseExited(final MouseEvent mouse) { + // Ignore + } + + /** + * Pass mouse events into the event queue. + * + * @param mouse mouse event received + */ + public void mousePressed(final MouseEvent mouse) { + int modifiers = mouse.getModifiersEx(); + boolean eventMouse1 = false; + boolean eventMouse2 = false; + boolean eventMouse3 = false; + if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { + eventMouse1 = true; + } + if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) { + eventMouse2 = true; + } + if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { + eventMouse3 = true; + } + mouse1 = eventMouse1; + mouse2 = eventMouse2; + mouse3 = eventMouse3; + int x = textColumn(mouse.getX()); + int y = textRow(mouse.getY()); + + TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN, + x, y, x, y, mouse1, mouse2, mouse3, false, false); + + synchronized (eventQueue) { + eventQueue.add(mouseEvent); + } + synchronized (listener) { + listener.notifyAll(); + } + } + + /** + * Pass mouse events into the event queue. + * + * @param mouse mouse event received + */ + public void mouseReleased(final MouseEvent mouse) { + int modifiers = mouse.getModifiersEx(); + boolean eventMouse1 = false; + boolean eventMouse2 = false; + boolean eventMouse3 = false; + if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { + eventMouse1 = true; + } + if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) { + eventMouse2 = true; + } + if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { + eventMouse3 = true; + } + if (mouse1) { + mouse1 = false; + eventMouse1 = true; + } + if (mouse2) { + mouse2 = false; + eventMouse2 = true; + } + if (mouse3) { + mouse3 = false; + eventMouse3 = true; + } + int x = textColumn(mouse.getX()); + int y = textRow(mouse.getY()); + + TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_UP, + x, y, x, y, eventMouse1, eventMouse2, eventMouse3, false, false); + + synchronized (eventQueue) { + eventQueue.add(mouseEvent); + } + synchronized (listener) { + listener.notifyAll(); + } + } + + /** + * Pass mouse events into the event queue. + * + * @param mouse mouse event received + */ + public void mouseWheelMoved(final MouseWheelEvent mouse) { + int modifiers = mouse.getModifiersEx(); + boolean eventMouse1 = false; + boolean eventMouse2 = false; + boolean eventMouse3 = false; + boolean mouseWheelUp = false; + boolean mouseWheelDown = false; + if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { + eventMouse1 = true; + } + if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) { + eventMouse2 = true; + } + if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { + eventMouse3 = true; + } + mouse1 = eventMouse1; + mouse2 = eventMouse2; + mouse3 = eventMouse3; + int x = textColumn(mouse.getX()); + int y = textRow(mouse.getY()); + if (mouse.getWheelRotation() > 0) { + mouseWheelDown = true; + } + if (mouse.getWheelRotation() < 0) { + mouseWheelUp = true; + } + + TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN, + x, y, x, y, mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown); + + synchronized (eventQueue) { + eventQueue.add(mouseEvent); + } + synchronized (listener) { + listener.notifyAll(); + } + } + +} diff --git a/src/jexer/session/TSessionInfo.java b/src/jexer/backend/TSessionInfo.java similarity index 99% rename from src/jexer/session/TSessionInfo.java rename to src/jexer/backend/TSessionInfo.java index adfdfd5..880098d 100644 --- a/src/jexer/session/TSessionInfo.java +++ b/src/jexer/backend/TSessionInfo.java @@ -26,7 +26,7 @@ * @author Kevin Lamonte [kevin.lamonte@gmail.com] * @version 1 */ -package jexer.session; +package jexer.backend; /** * TSessionInfo provides a default session implementation. The username is diff --git a/src/jexer/session/TTYSessionInfo.java b/src/jexer/backend/TTYSessionInfo.java similarity index 99% rename from src/jexer/session/TTYSessionInfo.java rename to src/jexer/backend/TTYSessionInfo.java index 5d20b6b..e69fc70 100644 --- a/src/jexer/session/TTYSessionInfo.java +++ b/src/jexer/backend/TTYSessionInfo.java @@ -26,7 +26,7 @@ * @author Kevin Lamonte [kevin.lamonte@gmail.com] * @version 1 */ -package jexer.session; +package jexer.backend; import java.io.BufferedReader; import java.io.InputStreamReader; diff --git a/src/jexer/session/package-info.java b/src/jexer/backend/TerminalReader.java similarity index 63% rename from src/jexer/session/package-info.java rename to src/jexer/backend/TerminalReader.java index 0b6ccf0..48e4043 100644 --- a/src/jexer/session/package-info.java +++ b/src/jexer/backend/TerminalReader.java @@ -26,8 +26,36 @@ * @author Kevin Lamonte [kevin.lamonte@gmail.com] * @version 1 */ +package jexer.backend; + +import java.util.List; + +import jexer.event.TInputEvent; /** - * Text terminal session values: width, height, username, language, etc. + * TerminalReader provides keyboard and mouse events. */ -package jexer.session; +public interface TerminalReader { + + /** + * Check if there are events in the queue. + * + * @return if true, getEvents() has something to return to the backend + */ + public boolean hasEvents(); + + /** + * Classes must provide an implementation to get keyboard, mouse, and + * screen resize events. + * + * @param queue list to append new events to + */ + public void getEvents(List queue); + + /** + * Classes must provide an implementation that closes sockets, restores + * console, etc. + */ + public void closeTerminal(); + +} diff --git a/src/jexer/demos/Demo5.java b/src/jexer/demos/Demo5.java new file mode 100644 index 0000000..88d6a7c --- /dev/null +++ b/src/jexer/demos/Demo5.java @@ -0,0 +1,177 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.demos; + +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JSplitPane; + +import jexer.backend.SwingBackend; + +/** + * This class is the main driver for a simple demonstration of Jexer's + * capabilities. It shows two Swing demo applications running in the same + * Swing UI. + */ +public class Demo5 implements WindowListener { + + /** + * The first demo application instance. + */ + DemoApplication app1 = null; + + /** + * The second demo application instance. + */ + DemoApplication app2 = null; + + /** + * Pass window events into the event queue. + * + * @param event window event received + */ + public void windowActivated(final WindowEvent event) { + // Ignore + } + + /** + * Pass window events into the event queue. + * + * @param event window event received + */ + public void windowClosed(final WindowEvent event) { + // Ignore + } + + /** + * Pass window events into the event queue. + * + * @param event window event received + */ + public void windowClosing(final WindowEvent event) { + if (app1 != null) { + app1.exit(); + } + if (app2 != null) { + app2.exit(); + } + } + + /** + * Pass window events into the event queue. + * + * @param event window event received + */ + public void windowDeactivated(final WindowEvent event) { + // Ignore + } + + /** + * Pass window events into the event queue. + * + * @param event window event received + */ + public void windowDeiconified(final WindowEvent event) { + // Ignore + } + + /** + * Pass window events into the event queue. + * + * @param event window event received + */ + public void windowIconified(final WindowEvent event) { + // Ignore + } + + /** + * Pass window events into the event queue. + * + * @param event window event received + */ + public void windowOpened(final WindowEvent event) { + // Ignore + } + + /** + * Run two demo applications in separate panes. + */ + private void addApplications() { + + // Spin up the frame + JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + frame.addWindowListener(this); + + // Create two panels with two applications, each with a different + // font size. + JPanel app1Panel = new JPanel(); + SwingBackend app1Backend = new SwingBackend(app1Panel, new Object(), + 80, 25, 16); + app1 = new DemoApplication(app1Backend); + app1Backend.setListener(app1); + + JPanel app2Panel = new JPanel(); + SwingBackend app2Backend = new SwingBackend(app2Panel, new Object(), + 80, 25, 18); + app2 = new DemoApplication(app2Backend); + app1Backend.setListener(app2); + (new Thread(app1)).start(); + (new Thread(app2)).start(); + + JSplitPane mainPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, + app1Panel, app2Panel); + mainPane.setOneTouchExpandable(true); + mainPane.setDividerLocation(500); + mainPane.setDividerSize(6); + mainPane.setBorder(null); + frame.setContentPane(mainPane); + + frame.setTitle("Two Jexer Apps In One Swing UI"); + frame.setSize(1000, 640); + frame.setVisible(true); + } + + /** + * Main entry point. + * + * @param args Command line arguments + */ + public static void main(final String [] args) { + try { + Demo5 demo = new Demo5(); + demo.addApplications(); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/src/jexer/demos/DemoApplication.java b/src/jexer/demos/DemoApplication.java index a352dee..43bb709 100644 --- a/src/jexer/demos/DemoApplication.java +++ b/src/jexer/demos/DemoApplication.java @@ -34,6 +34,8 @@ import java.util.*; import jexer.*; import jexer.event.*; import jexer.menu.*; +import jexer.backend.Backend; +import jexer.backend.SwingTerminal; /** * The demo application itself. @@ -73,6 +75,12 @@ public class DemoApplication extends TApplication { item.setEnabled(false); item = subMenu.addItem(2002, "&Normal (sub)"); + if (getScreen() instanceof SwingTerminal) { + TMenu swingMenu = addMenu("&Swing"); + item = swingMenu.addItem(3000, "&Bigger +2"); + item = swingMenu.addItem(3001, "&Smaller -2"); + } + addWindowMenu(); addHelpMenu(); } @@ -133,6 +141,17 @@ public class DemoApplication extends TApplication { this(input, reader, writer, false); } + /** + * Public constructor. + * + * @param backend a Backend that is already ready to go. + */ + public DemoApplication(final Backend backend) { + super(backend); + + addAllWidgets(); + } + /** * Handle menu events. * @@ -143,6 +162,21 @@ public class DemoApplication extends TApplication { @Override public boolean onMenu(final TMenuEvent menu) { + if (menu.getId() == 3000) { + // Bigger +2 + assert (getScreen() instanceof SwingTerminal); + SwingTerminal terminal = (SwingTerminal) getScreen(); + terminal.setFontSize(terminal.getFontSize() + 2); + return true; + } + if (menu.getId() == 3001) { + // Smaller -2 + assert (getScreen() instanceof SwingTerminal); + SwingTerminal terminal = (SwingTerminal) getScreen(); + terminal.setFontSize(terminal.getFontSize() - 2); + return true; + } + if (menu.getId() == 2050) { new TEditColorThemeWindow(this); return true; diff --git a/src/jexer/io/ECMA48Screen.java b/src/jexer/io/ECMA48Screen.java deleted file mode 100644 index 9daf6de..0000000 --- a/src/jexer/io/ECMA48Screen.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Jexer - Java Text User Interface - * - * The MIT License (MIT) - * - * Copyright (C) 2017 Kevin Lamonte - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - * @author Kevin Lamonte [kevin.lamonte@gmail.com] - * @version 1 - */ -package jexer.io; - -import jexer.bits.Cell; -import jexer.bits.CellAttributes; - -/** - * This Screen implementation draws to an xterm/ANSI X3.64/ECMA-48 type - * terminal. - */ -public final class ECMA48Screen extends Screen { - - /** - * Emit debugging to stderr. - */ - private boolean debugToStderr = false; - - /** - * We call terminal.cursor() so need the instance. - */ - private ECMA48Terminal terminal; - - /** - * Public constructor. - * - * @param terminal ECMA48Terminal to use - */ - public ECMA48Screen(final ECMA48Terminal terminal) { - this.terminal = terminal; - - // Query the screen size - setDimensions(terminal.getSessionInfo().getWindowWidth(), - terminal.getSessionInfo().getWindowHeight()); - } - - /** - * Perform a somewhat-optimal rendering of a line. - * - * @param y row coordinate. 0 is the top-most row. - * @param sb StringBuilder to write escape sequences to - * @param lastAttr cell attributes from the last call to flushLine - */ - private void flushLine(final int y, final StringBuilder sb, - CellAttributes lastAttr) { - - int lastX = -1; - int textEnd = 0; - for (int x = 0; x < width; x++) { - Cell lCell = logical[x][y]; - if (!lCell.isBlank()) { - textEnd = x; - } - } - // Push textEnd to first column beyond the text area - textEnd++; - - // DEBUG - // reallyCleared = true; - - for (int x = 0; x < width; x++) { - Cell lCell = logical[x][y]; - Cell pCell = physical[x][y]; - - if (!lCell.equals(pCell) || reallyCleared) { - - if (debugToStderr) { - System.err.printf("\n--\n"); - System.err.printf(" Y: %d X: %d\n", y, x); - System.err.printf(" lCell: %s\n", lCell); - System.err.printf(" pCell: %s\n", pCell); - System.err.printf(" ==== \n"); - } - - if (lastAttr == null) { - lastAttr = new CellAttributes(); - sb.append(terminal.normal()); - } - - // Place the cell - if ((lastX != (x - 1)) || (lastX == -1)) { - // Advancing at least one cell, or the first gotoXY - sb.append(terminal.gotoXY(x, y)); - } - - assert (lastAttr != null); - - if ((x == textEnd) && (textEnd < width - 1)) { - assert (lCell.isBlank()); - - for (int i = x; i < width; i++) { - assert (logical[i][y].isBlank()); - // Physical is always updated - physical[i][y].reset(); - } - - // Clear remaining line - sb.append(terminal.clearRemainingLine()); - lastAttr.reset(); - return; - } - - // Now emit only the modified attributes - if ((lCell.getForeColor() != lastAttr.getForeColor()) - && (lCell.getBackColor() != lastAttr.getBackColor()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - // Both colors changed, attributes the same - sb.append(terminal.color(lCell.isBold(), - lCell.getForeColor(), lCell.getBackColor())); - - if (debugToStderr) { - System.err.printf("1 Change only fore/back colors\n"); - } - } else if ((lCell.getForeColor() != lastAttr.getForeColor()) - && (lCell.getBackColor() != lastAttr.getBackColor()) - && (lCell.isBold() != lastAttr.isBold()) - && (lCell.isReverse() != lastAttr.isReverse()) - && (lCell.isUnderline() != lastAttr.isUnderline()) - && (lCell.isBlink() != lastAttr.isBlink()) - ) { - // Everything is different - sb.append(terminal.color(lCell.getForeColor(), - lCell.getBackColor(), - lCell.isBold(), lCell.isReverse(), - lCell.isBlink(), - lCell.isUnderline())); - - if (debugToStderr) { - System.err.printf("2 Set all attributes\n"); - } - } else if ((lCell.getForeColor() != lastAttr.getForeColor()) - && (lCell.getBackColor() == lastAttr.getBackColor()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - - // Attributes same, foreColor different - sb.append(terminal.color(lCell.isBold(), - lCell.getForeColor(), true)); - - if (debugToStderr) { - System.err.printf("3 Change foreColor\n"); - } - } else if ((lCell.getForeColor() == lastAttr.getForeColor()) - && (lCell.getBackColor() != lastAttr.getBackColor()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - // Attributes same, backColor different - sb.append(terminal.color(lCell.isBold(), - lCell.getBackColor(), false)); - - if (debugToStderr) { - System.err.printf("4 Change backColor\n"); - } - } else if ((lCell.getForeColor() == lastAttr.getForeColor()) - && (lCell.getBackColor() == lastAttr.getBackColor()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - - // All attributes the same, just print the char - // NOP - - if (debugToStderr) { - System.err.printf("5 Only emit character\n"); - } - } else { - // Just reset everything again - sb.append(terminal.color(lCell.getForeColor(), - lCell.getBackColor(), - lCell.isBold(), - lCell.isReverse(), - lCell.isBlink(), - lCell.isUnderline())); - - if (debugToStderr) { - System.err.printf("6 Change all attributes\n"); - } - } - // Emit the character - sb.append(lCell.getChar()); - - // Save the last rendered cell - lastX = x; - lastAttr.setTo(lCell); - - // Physical is always updated - physical[x][y].setTo(lCell); - - } // if (!lCell.equals(pCell) || (reallyCleared == true)) - - } // for (int x = 0; x < width; x++) - } - - /** - * Render the screen to a string that can be emitted to something that - * knows how to process ECMA-48/ANSI X3.64 escape sequences. - * - * @return escape sequences string that provides the updates to the - * physical screen - */ - private String flushString() { - if (!dirty) { - assert (!reallyCleared); - return ""; - } - - CellAttributes attr = null; - - StringBuilder sb = new StringBuilder(); - if (reallyCleared) { - attr = new CellAttributes(); - sb.append(terminal.clearAll()); - } - - for (int y = 0; y < height; y++) { - flushLine(y, sb, attr); - } - - dirty = false; - reallyCleared = false; - - String result = sb.toString(); - if (debugToStderr) { - System.err.printf("flushString(): %s\n", result); - } - return result; - } - - /** - * Push the logical screen to the physical device. - */ - @Override - public void flushPhysical() { - String result = flushString(); - if ((cursorVisible) - && (cursorY <= height - 1) - && (cursorX <= width - 1) - ) { - result += terminal.cursor(true); - result += terminal.gotoXY(cursorX, cursorY); - } else { - result += terminal.cursor(false); - } - terminal.getOutput().write(result); - terminal.flush(); - } - - /** - * Set the window title. - * - * @param title the new title - */ - public void setTitle(final String title) { - terminal.getOutput().write(terminal.setTitle(title)); - terminal.flush(); - } - -} diff --git a/src/jexer/io/SwingScreen.java b/src/jexer/io/SwingScreen.java deleted file mode 100644 index ee8467d..0000000 --- a/src/jexer/io/SwingScreen.java +++ /dev/null @@ -1,997 +0,0 @@ -/* - * Jexer - Java Text User Interface - * - * The MIT License (MIT) - * - * Copyright (C) 2017 Kevin Lamonte - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - * @author Kevin Lamonte [kevin.lamonte@gmail.com] - * @version 1 - */ -package jexer.io; - -import java.awt.Color; -import java.awt.Cursor; -import java.awt.Font; -import java.awt.FontMetrics; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Insets; -import java.awt.Point; -import java.awt.Rectangle; -import java.awt.Toolkit; -import java.awt.geom.Rectangle2D; -import java.awt.image.BufferedImage; -import java.awt.image.BufferStrategy; -import java.io.InputStream; -import java.util.Date; -import java.util.HashMap; -import javax.swing.JFrame; -import javax.swing.SwingUtilities; - -import jexer.bits.Cell; -import jexer.bits.CellAttributes; -import jexer.session.SwingSessionInfo; - -/** - * This Screen implementation draws to a Java Swing JFrame. - */ -public final class SwingScreen extends Screen { - - /** - * If true, use triple buffering thread. - */ - private static boolean tripleBuffer = true; - - /** - * Cursor style to draw. - */ - public enum CursorStyle { - /** - * Use an underscore for the cursor. - */ - UNDERLINE, - - /** - * Use a solid block for the cursor. - */ - BLOCK, - - /** - * Use an outlined block for the cursor. - */ - OUTLINE - } - - private static Color MYBLACK; - private static Color MYRED; - private static Color MYGREEN; - private static Color MYYELLOW; - private static Color MYBLUE; - private static Color MYMAGENTA; - private static Color MYCYAN; - private static Color MYWHITE; - - private static Color MYBOLD_BLACK; - private static Color MYBOLD_RED; - private static Color MYBOLD_GREEN; - private static Color MYBOLD_YELLOW; - private static Color MYBOLD_BLUE; - private static Color MYBOLD_MAGENTA; - private static Color MYBOLD_CYAN; - private static Color MYBOLD_WHITE; - - private static boolean dosColors = false; - - /** - * Setup Swing colors to match DOS color palette. - */ - private static void setDOSColors() { - if (dosColors) { - return; - } - MYBLACK = new Color(0x00, 0x00, 0x00); - MYRED = new Color(0xa8, 0x00, 0x00); - MYGREEN = new Color(0x00, 0xa8, 0x00); - MYYELLOW = new Color(0xa8, 0x54, 0x00); - MYBLUE = new Color(0x00, 0x00, 0xa8); - MYMAGENTA = new Color(0xa8, 0x00, 0xa8); - MYCYAN = new Color(0x00, 0xa8, 0xa8); - MYWHITE = new Color(0xa8, 0xa8, 0xa8); - MYBOLD_BLACK = new Color(0x54, 0x54, 0x54); - MYBOLD_RED = new Color(0xfc, 0x54, 0x54); - MYBOLD_GREEN = new Color(0x54, 0xfc, 0x54); - MYBOLD_YELLOW = new Color(0xfc, 0xfc, 0x54); - MYBOLD_BLUE = new Color(0x54, 0x54, 0xfc); - MYBOLD_MAGENTA = new Color(0xfc, 0x54, 0xfc); - MYBOLD_CYAN = new Color(0x54, 0xfc, 0xfc); - MYBOLD_WHITE = new Color(0xfc, 0xfc, 0xfc); - - dosColors = true; - } - - /** - * SwingFrame is our top-level hook into the Swing system. - */ - class SwingFrame extends JFrame { - - /** - * Serializable version. - */ - private static final long serialVersionUID = 1; - - /** - * The terminus font resource filename. - */ - private static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf"; - - /** - * The BufferStrategy object needed for triple-buffering. - */ - private BufferStrategy bufferStrategy; - - /** - * A cache of previously-rendered glyphs for blinking text, when it - * is not visible. - */ - private HashMap glyphCacheBlink; - - /** - * A cache of previously-rendered glyphs for non-blinking, or - * blinking-and-visible, text. - */ - private HashMap glyphCache; - - /** - * The TUI Screen data. - */ - SwingScreen screen; - - /** - * If true, we were successful getting Terminus. - */ - private boolean gotTerminus = false; - - /** - * Width of a character cell. - */ - private int textWidth = 1; - - /** - * Height of a character cell. - */ - private int textHeight = 1; - - /** - * Descent of a character cell. - */ - private int maxDescent = 0; - - /** - * System-dependent Y adjustment for text in the character cell. - */ - private int textAdjustY = 0; - - /** - * System-dependent X adjustment for text in the character cell. - */ - private int textAdjustX = 0; - - /** - * Top pixel absolute location. - */ - private int top = 30; - - /** - * Left pixel absolute location. - */ - private int left = 30; - - /** - * The cursor style to draw. - */ - private CursorStyle cursorStyle = CursorStyle.UNDERLINE; - - /** - * The number of millis to wait before switching the blink from - * visible to invisible. - */ - private long blinkMillis = 500; - - /** - * If true, the cursor should be visible right now based on the blink - * time. - */ - private boolean cursorBlinkVisible = true; - - /** - * The time that the blink last flipped from visible to invisible or - * from invisible to visible. - */ - private long lastBlinkTime = 0; - - /** - * Convert a CellAttributes foreground color to an Swing Color. - * - * @param attr the text attributes - * @return the Swing Color - */ - private Color attrToForegroundColor(final CellAttributes attr) { - if (attr.isBold()) { - if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) { - return MYBOLD_BLACK; - } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) { - return MYBOLD_RED; - } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) { - return MYBOLD_BLUE; - } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) { - return MYBOLD_GREEN; - } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) { - return MYBOLD_YELLOW; - } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) { - return MYBOLD_CYAN; - } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) { - return MYBOLD_MAGENTA; - } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) { - return MYBOLD_WHITE; - } - } else { - if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) { - return MYBLACK; - } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) { - return MYRED; - } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) { - return MYBLUE; - } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) { - return MYGREEN; - } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) { - return MYYELLOW; - } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) { - return MYCYAN; - } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) { - return MYMAGENTA; - } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) { - return MYWHITE; - } - } - throw new IllegalArgumentException("Invalid color: " + - attr.getForeColor().getValue()); - } - - /** - * Convert a CellAttributes background color to an Swing Color. - * - * @param attr the text attributes - * @return the Swing Color - */ - private Color attrToBackgroundColor(final CellAttributes attr) { - if (attr.getBackColor().equals(jexer.bits.Color.BLACK)) { - return MYBLACK; - } else if (attr.getBackColor().equals(jexer.bits.Color.RED)) { - return MYRED; - } else if (attr.getBackColor().equals(jexer.bits.Color.BLUE)) { - return MYBLUE; - } else if (attr.getBackColor().equals(jexer.bits.Color.GREEN)) { - return MYGREEN; - } else if (attr.getBackColor().equals(jexer.bits.Color.YELLOW)) { - return MYYELLOW; - } else if (attr.getBackColor().equals(jexer.bits.Color.CYAN)) { - return MYCYAN; - } else if (attr.getBackColor().equals(jexer.bits.Color.MAGENTA)) { - return MYMAGENTA; - } else if (attr.getBackColor().equals(jexer.bits.Color.WHITE)) { - return MYWHITE; - } - throw new IllegalArgumentException("Invalid color: " + - attr.getBackColor().getValue()); - } - - /** - * Public constructor. - * - * @param screen the Screen that Backend talks to - * @param fontSize the size in points. Good values to pick are: 16, - * 20, 22, and 24. - */ - public SwingFrame(final SwingScreen screen, final int fontSize) { - this.screen = screen; - setDOSColors(); - - // Figure out my cursor style - String cursorStyleString = System.getProperty( - "jexer.Swing.cursorStyle", "underline").toLowerCase(); - - if (cursorStyleString.equals("underline")) { - cursorStyle = CursorStyle.UNDERLINE; - } else if (cursorStyleString.equals("outline")) { - cursorStyle = CursorStyle.OUTLINE; - } else if (cursorStyleString.equals("block")) { - cursorStyle = CursorStyle.BLOCK; - } - - if (System.getProperty("jexer.Swing.tripleBuffer") != null) { - if (System.getProperty("jexer.Swing.tripleBuffer"). - equals("false")) { - - SwingScreen.tripleBuffer = false; - } - } - - setTitle("Jexer Application"); - setBackground(Color.black); - - try { - // Always try to use Terminus, the one decent font. - ClassLoader loader = Thread.currentThread(). - getContextClassLoader(); - InputStream in = loader.getResourceAsStream(FONTFILE); - Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in); - Font terminus = terminusRoot.deriveFont(Font.PLAIN, fontSize); - setFont(terminus); - gotTerminus = true; - } catch (Exception e) { - e.printStackTrace(); - // setFont(new Font("Liberation Mono", Font.PLAIN, 24)); - setFont(new Font(Font.MONOSPACED, Font.PLAIN, fontSize)); - } - pack(); - - // Kill the X11 cursor - // Transparent 16 x 16 pixel cursor image. - BufferedImage cursorImg = new BufferedImage(16, 16, - BufferedImage.TYPE_INT_ARGB); - // Create a new blank cursor. - Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor( - cursorImg, new Point(0, 0), "blank cursor"); - setCursor(blankCursor); - - // Be capable of seeing Tab / Shift-Tab - setFocusTraversalKeysEnabled(false); - - // Save the text cell width/height - getFontDimensions(); - - // Cache glyphs as they are rendered - glyphCacheBlink = new HashMap(); - glyphCache = new HashMap(); - - // Setup triple-buffering - if (SwingScreen.tripleBuffer) { - setIgnoreRepaint(true); - createBufferStrategy(3); - bufferStrategy = getBufferStrategy(); - } - } - - /** - * Figure out what textAdjustX and textAdjustY should be, based on - * the location of a vertical bar (to find textAdjustY) and a - * horizontal bar (to find textAdjustX). - * - * @return true if textAdjustX and textAdjustY were guessed at - * correctly - */ - private boolean getFontAdjustments() { - BufferedImage image = null; - - // What SHOULD happen is that the topmost/leftmost white pixel is - // at position (gr2x, gr2y). But it might also be off by a pixel - // in either direction. - - Graphics2D gr2 = null; - int gr2x = 3; - int gr2y = 3; - image = new BufferedImage(textWidth * 2, textHeight * 2, - BufferedImage.TYPE_INT_ARGB); - - gr2 = image.createGraphics(); - gr2.setFont(getFont()); - gr2.setColor(java.awt.Color.BLACK); - gr2.fillRect(0, 0, textWidth * 2, textHeight * 2); - gr2.setColor(java.awt.Color.WHITE); - char [] chars = new char[1]; - chars[0] = jexer.bits.GraphicsChars.VERTICAL_BAR; - gr2.drawChars(chars, 0, 1, gr2x, gr2y + textHeight - maxDescent); - gr2.dispose(); - - for (int x = 0; x < textWidth; x++) { - for (int y = 0; y < textHeight; y++) { - - /* - System.err.println("X: " + x + " Y: " + y + " " + - image.getRGB(x, y)); - */ - - if ((image.getRGB(x, y) & 0xFFFFFF) != 0) { - textAdjustY = (gr2y - y); - - // System.err.println("textAdjustY: " + textAdjustY); - x = textWidth; - break; - } - } - } - - gr2 = image.createGraphics(); - gr2.setFont(getFont()); - gr2.setColor(java.awt.Color.BLACK); - gr2.fillRect(0, 0, textWidth * 2, textHeight * 2); - gr2.setColor(java.awt.Color.WHITE); - chars[0] = jexer.bits.GraphicsChars.SINGLE_BAR; - gr2.drawChars(chars, 0, 1, gr2x, gr2y + textHeight - maxDescent); - gr2.dispose(); - - for (int x = 0; x < textWidth; x++) { - for (int y = 0; y < textHeight; y++) { - - /* - System.err.println("X: " + x + " Y: " + y + " " + - image.getRGB(x, y)); - */ - - if ((image.getRGB(x, y) & 0xFFFFFF) != 0) { - textAdjustX = (gr2x - x); - - // System.err.println("textAdjustX: " + textAdjustX); - return true; - } - } - } - - // Something weird happened, don't rely on this function. - // System.err.println("getFontAdjustments: false"); - return false; - } - - - /** - * Figure out my font dimensions. - */ - private void getFontDimensions() { - Graphics gr = getGraphics(); - FontMetrics fm = gr.getFontMetrics(); - maxDescent = fm.getMaxDescent(); - Rectangle2D bounds = fm.getMaxCharBounds(gr); - int leading = fm.getLeading(); - textWidth = (int)Math.round(bounds.getWidth()); - // textHeight = (int)Math.round(bounds.getHeight()) - maxDescent; - - // This produces the same number, but works better for ugly - // monospace. - textHeight = fm.getMaxAscent() + maxDescent - leading; - - if (gotTerminus == true) { - textHeight++; - } - - if (getFontAdjustments() == false) { - // We were unable to programmatically determine textAdjustX - // and textAdjustY, so try some guesses based on VM vendor. - String runtime = System.getProperty("java.runtime.name"); - if ((runtime != null) && (runtime.contains("Java(TM)"))) { - textAdjustY = -1; - textAdjustX = 0; - } - } - } - - /** - * Resize to font dimensions. - */ - public void resizeToScreen() { - // Figure out the thickness of borders and use that to set the - // final size. - Insets insets = getInsets(); - left = insets.left; - top = insets.top; - - setSize(textWidth * screen.width + insets.left + insets.right, - textHeight * screen.height + insets.top + insets.bottom); - } - - /** - * Update redraws the whole screen. - * - * @param gr the Swing Graphics context - */ - @Override - public void update(final Graphics gr) { - // The default update clears the area. Don't do that, instead - // just paint it directly. - paint(gr); - } - - /** - * Draw one glyph to the screen. - * - * @param gr the Swing Graphics context - * @param cell the Cell to draw - * @param xPixel the x-coordinate to render to. 0 means the - * left-most pixel column. - * @param yPixel the y-coordinate to render to. 0 means the top-most - * pixel row. - */ - private void drawGlyph(final Graphics gr, final Cell cell, - final int xPixel, final int yPixel) { - - BufferedImage image = null; - if (cell.isBlink() && !cursorBlinkVisible) { - image = glyphCacheBlink.get(cell); - } else { - image = glyphCache.get(cell); - } - if (image != null) { - gr.drawImage(image, xPixel, yPixel, this); - return; - } - - // Generate glyph and draw it. - Graphics2D gr2 = null; - int gr2x = xPixel; - int gr2y = yPixel; - if (tripleBuffer) { - image = new BufferedImage(textWidth, textHeight, - BufferedImage.TYPE_INT_ARGB); - gr2 = image.createGraphics(); - gr2.setFont(getFont()); - gr2x = 0; - gr2y = 0; - } else { - gr2 = (Graphics2D) gr; - } - - Cell cellColor = new Cell(); - cellColor.setTo(cell); - - // Check for reverse - if (cell.isReverse()) { - cellColor.setForeColor(cell.getBackColor()); - cellColor.setBackColor(cell.getForeColor()); - } - - // Draw the background rectangle, then the foreground character. - gr2.setColor(attrToBackgroundColor(cellColor)); - gr2.fillRect(gr2x, gr2y, textWidth, textHeight); - - // Handle blink and underline - if (!cell.isBlink() - || (cell.isBlink() && cursorBlinkVisible) - ) { - gr2.setColor(attrToForegroundColor(cellColor)); - char [] chars = new char[1]; - chars[0] = cell.getChar(); - gr2.drawChars(chars, 0, 1, gr2x + textAdjustX, - gr2y + textHeight - maxDescent + textAdjustY); - - if (cell.isUnderline()) { - gr2.fillRect(gr2x, gr2y + textHeight - 2, textWidth, 2); - } - } - - if (tripleBuffer) { - gr2.dispose(); - - // We need a new key that will not be mutated by - // invertCell(). - Cell key = new Cell(); - key.setTo(cell); - if (cell.isBlink() && !cursorBlinkVisible) { - glyphCacheBlink.put(key, image); - } else { - glyphCache.put(key, image); - } - - gr.drawImage(image, xPixel, yPixel, this); - } - - } - - /** - * Check if the cursor is visible, and if so draw it. - * - * @param gr the Swing Graphics context - */ - private void drawCursor(final Graphics gr) { - - if (cursorVisible - && (cursorY <= screen.height - 1) - && (cursorX <= screen.width - 1) - && cursorBlinkVisible - ) { - int xPixel = cursorX * textWidth + left; - int yPixel = cursorY * textHeight + top; - Cell lCell = screen.logical[cursorX][cursorY]; - gr.setColor(attrToForegroundColor(lCell)); - switch (cursorStyle) { - default: - // Fall through... - case UNDERLINE: - gr.fillRect(xPixel, yPixel + textHeight - 2, textWidth, 2); - break; - case BLOCK: - gr.fillRect(xPixel, yPixel, textWidth, textHeight); - break; - case OUTLINE: - gr.drawRect(xPixel, yPixel, textWidth - 1, textHeight - 1); - break; - } - } - } - - /** - * Paint redraws the whole screen. - * - * @param gr the Swing Graphics context - */ - @Override - public void paint(final Graphics gr) { - // Do nothing until the screen reference has been set. - if (screen == null) { - return; - } - if (screen.frame == null) { - return; - } - - // See if it is time to flip the blink time. - long nowTime = (new Date()).getTime(); - if (nowTime > blinkMillis + lastBlinkTime) { - lastBlinkTime = nowTime; - cursorBlinkVisible = !cursorBlinkVisible; - } - - int xCellMin = 0; - int xCellMax = screen.width; - int yCellMin = 0; - int yCellMax = screen.height; - - Rectangle bounds = gr.getClipBounds(); - if (bounds != null) { - // Only update what is in the bounds - xCellMin = screen.textColumn(bounds.x); - xCellMax = screen.textColumn(bounds.x + bounds.width); - if (xCellMax > screen.width) { - xCellMax = screen.width; - } - if (xCellMin >= xCellMax) { - xCellMin = xCellMax - 2; - } - if (xCellMin < 0) { - xCellMin = 0; - } - yCellMin = screen.textRow(bounds.y); - yCellMax = screen.textRow(bounds.y + bounds.height); - if (yCellMax > screen.height) { - yCellMax = screen.height; - } - if (yCellMin >= yCellMax) { - yCellMin = yCellMax - 2; - } - if (yCellMin < 0) { - yCellMin = 0; - } - } else { - // We need a total repaint - reallyCleared = true; - } - - // Prevent updates to the screen's data from the TApplication - // threads. - synchronized (screen) { - /* - System.err.printf("bounds %s X %d %d Y %d %d\n", - bounds, xCellMin, xCellMax, yCellMin, yCellMax); - */ - - for (int y = yCellMin; y < yCellMax; y++) { - for (int x = xCellMin; x < xCellMax; x++) { - - int xPixel = x * textWidth + left; - int yPixel = y * textHeight + top; - - Cell lCell = screen.logical[x][y]; - Cell pCell = screen.physical[x][y]; - - if (!lCell.equals(pCell) - || lCell.isBlink() - || reallyCleared) { - - drawGlyph(gr, lCell, xPixel, yPixel); - - // Physical is always updated - physical[x][y].setTo(lCell); - } - } - } - drawCursor(gr); - - dirty = false; - reallyCleared = false; - } // synchronized (screen) - } - - } // class SwingFrame - - /** - * The raw Swing JFrame. Note package private access. - */ - SwingFrame frame; - - /** - * Restore terminal to normal state. - */ - public void shutdown() { - frame.dispose(); - } - - /** - * Public constructor. - * - * @param windowWidth the number of text columns to start with - * @param windowHeight the number of text rows to start with - * @param fontSize the size in points. Good values to pick are: 16, 20, - * 22, and 24. - */ - public SwingScreen(final int windowWidth, final int windowHeight, - final int fontSize) { - - try { - SwingUtilities.invokeAndWait(new Runnable() { - public void run() { - SwingScreen.this.frame = new SwingFrame(SwingScreen.this, - fontSize); - SwingScreen.this.sessionInfo = - new SwingSessionInfo(SwingScreen.this.frame, - frame.textWidth, frame.textHeight, - windowWidth, windowHeight); - - SwingScreen.this.setDimensions(sessionInfo.getWindowWidth(), - sessionInfo.getWindowHeight()); - - SwingScreen.this.frame.resizeToScreen(); - SwingScreen.this.frame.setVisible(true); - } - }); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * The sessionInfo. - */ - private SwingSessionInfo sessionInfo; - - /** - * Create the SwingSessionInfo. Note package private access. - * - * @return the sessionInfo - */ - SwingSessionInfo getSessionInfo() { - return sessionInfo; - } - - /** - * Push the logical screen to the physical device. - */ - @Override - public void flushPhysical() { - - /* - System.err.printf("flushPhysical(): reallyCleared %s dirty %s\n", - reallyCleared, dirty); - */ - - // If reallyCleared is set, we have to draw everything. - if ((frame.bufferStrategy != null) && (reallyCleared == true)) { - // Triple-buffering: we have to redraw everything on this thread. - Graphics gr = frame.bufferStrategy.getDrawGraphics(); - frame.paint(gr); - gr.dispose(); - frame.bufferStrategy.show(); - // sync() doesn't seem to help the tearing for me. - // Toolkit.getDefaultToolkit().sync(); - return; - } else if ((frame.bufferStrategy == null) && (reallyCleared == true)) { - // Repaint everything on the Swing thread. - frame.repaint(); - return; - } - - // Do nothing if nothing happened. - if (!dirty) { - return; - } - - if (frame.bufferStrategy != null) { - // See if it is time to flip the blink time. - long nowTime = (new Date()).getTime(); - if (nowTime > frame.blinkMillis + frame.lastBlinkTime) { - frame.lastBlinkTime = nowTime; - frame.cursorBlinkVisible = !frame.cursorBlinkVisible; - } - - Graphics gr = frame.bufferStrategy.getDrawGraphics(); - - synchronized (this) { - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - Cell lCell = logical[x][y]; - Cell pCell = physical[x][y]; - - int xPixel = x * frame.textWidth + frame.left; - int yPixel = y * frame.textHeight + frame.top; - - if (!lCell.equals(pCell) - || ((x == cursorX) - && (y == cursorY) - && cursorVisible) - || (lCell.isBlink()) - ) { - frame.drawGlyph(gr, lCell, xPixel, yPixel); - physical[x][y].setTo(lCell); - } - } - } - frame.drawCursor(gr); - } // synchronized (this) - - gr.dispose(); - frame.bufferStrategy.show(); - // sync() doesn't seem to help the tearing for me. - // Toolkit.getDefaultToolkit().sync(); - return; - } - - // Swing thread version: request a repaint, but limit it to the area - // that has changed. - - // Find the minimum-size damaged region. - int xMin = frame.getWidth(); - int xMax = 0; - int yMin = frame.getHeight(); - int yMax = 0; - - synchronized (this) { - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - Cell lCell = logical[x][y]; - Cell pCell = physical[x][y]; - - int xPixel = x * frame.textWidth + frame.left; - int yPixel = y * frame.textHeight + frame.top; - - if (!lCell.equals(pCell) - || ((x == cursorX) - && (y == cursorY) - && cursorVisible) - || lCell.isBlink() - ) { - if (xPixel < xMin) { - xMin = xPixel; - } - if (xPixel + frame.textWidth > xMax) { - xMax = xPixel + frame.textWidth; - } - if (yPixel < yMin) { - yMin = yPixel; - } - if (yPixel + frame.textHeight > yMax) { - yMax = yPixel + frame.textHeight; - } - } - } - } - } - if (xMin + frame.textWidth >= xMax) { - xMax += frame.textWidth; - } - if (yMin + frame.textHeight >= yMax) { - yMax += frame.textHeight; - } - - // Repaint the desired area - /* - System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax, - yMin, yMax); - */ - if (frame.bufferStrategy != null) { - // This path should never be taken, but is left here for - // completeness. - Graphics gr = frame.bufferStrategy.getDrawGraphics(); - Rectangle bounds = new Rectangle(xMin, yMin, xMax - xMin, - yMax - yMin); - gr.setClip(bounds); - frame.paint(gr); - gr.dispose(); - frame.bufferStrategy.show(); - // sync() doesn't seem to help the tearing for me. - // Toolkit.getDefaultToolkit().sync(); - } else { - // Repaint on the Swing thread. - frame.repaint(xMin, yMin, xMax - xMin, yMax - yMin); - } - } - - /** - * Put the cursor at (x,y). - * - * @param visible if true, the cursor should be visible - * @param x column coordinate to put the cursor on - * @param y row coordinate to put the cursor on - */ - @Override - public void putCursor(final boolean visible, final int x, final int y) { - - if ((visible == cursorVisible) && ((x == cursorX) && (y == cursorY))) { - // See if it is time to flip the blink time. - long nowTime = (new Date()).getTime(); - if (nowTime < frame.blinkMillis + frame.lastBlinkTime) { - // Nothing has changed, so don't do anything. - return; - } - } - - if (cursorVisible - && (cursorY <= height - 1) - && (cursorX <= width - 1) - ) { - // Make the current cursor position dirty - if (physical[cursorX][cursorY].getChar() == 'Q') { - physical[cursorX][cursorY].setChar('X'); - } else { - physical[cursorX][cursorY].setChar('Q'); - } - } - - super.putCursor(visible, x, y); - } - - /** - * Convert pixel column position to text cell column position. - * - * @param x pixel column position - * @return text cell column position - */ - public int textColumn(final int x) { - return ((x - frame.left) / frame.textWidth); - } - - /** - * Convert pixel row position to text cell row position. - * - * @param y pixel row position - * @return text cell row position - */ - public int textRow(final int y) { - return ((y - frame.top) / frame.textHeight); - } - - /** - * Set the window title. - * - * @param title the new title - */ - public void setTitle(final String title) { - frame.setTitle(title); - } - -} diff --git a/src/jexer/io/SwingTerminal.java b/src/jexer/io/SwingTerminal.java deleted file mode 100644 index 07d1ffa..0000000 --- a/src/jexer/io/SwingTerminal.java +++ /dev/null @@ -1,721 +0,0 @@ -/* - * Jexer - Java Text User Interface - * - * The MIT License (MIT) - * - * Copyright (C) 2017 Kevin Lamonte - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - * @author Kevin Lamonte [kevin.lamonte@gmail.com] - * @version 1 - */ -package jexer.io; - -import java.awt.event.ComponentEvent; -import java.awt.event.ComponentListener; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.MouseMotionListener; -import java.awt.event.MouseWheelEvent; -import java.awt.event.MouseWheelListener; -import java.awt.event.WindowEvent; -import java.awt.event.WindowListener; -import java.util.List; -import java.util.LinkedList; - -import jexer.TKeypress; -import jexer.event.TCommandEvent; -import jexer.event.TInputEvent; -import jexer.event.TKeypressEvent; -import jexer.event.TMouseEvent; -import jexer.event.TResizeEvent; -import jexer.session.SessionInfo; -import jexer.session.SwingSessionInfo; -import static jexer.TCommand.*; -import static jexer.TKeypress.*; - -/** - * This class reads keystrokes and mouse events from an Swing JFrame. - */ -public final class SwingTerminal implements ComponentListener, KeyListener, - MouseListener, MouseMotionListener, - MouseWheelListener, WindowListener { - - /** - * The backend Screen. - */ - private SwingScreen screen; - - /** - * The session information. - */ - private SwingSessionInfo sessionInfo; - - /** - * Getter for sessionInfo. - * - * @return the SessionInfo - */ - public SessionInfo getSessionInfo() { - return sessionInfo; - } - - /** - * The listening object that run() wakes up on new input. - */ - private Object listener; - - /** - * The event queue, filled up by a thread reading on input. - */ - private List eventQueue; - - /** - * The last reported mouse X position. - */ - private int oldMouseX = -1; - - /** - * The last reported mouse Y position. - */ - private int oldMouseY = -1; - - /** - * true if mouse1 was down. Used to report mouse1 on the release event. - */ - private boolean mouse1 = false; - - /** - * true if mouse2 was down. Used to report mouse2 on the release event. - */ - private boolean mouse2 = false; - - /** - * true if mouse3 was down. Used to report mouse3 on the release event. - */ - private boolean mouse3 = false; - - /** - * Check if there are events in the queue. - * - * @return if true, getEvents() has something to return to the backend - */ - public boolean hasEvents() { - synchronized (eventQueue) { - return (eventQueue.size() > 0); - } - } - - /** - * Constructor sets up state for getEvent(). - * - * @param listener the object this backend needs to wake up when new - * input comes in - * @param screen the top-level Swing frame - */ - public SwingTerminal(final Object listener, final SwingScreen screen) { - this.listener = listener; - this.screen = screen; - mouse1 = false; - mouse2 = false; - mouse3 = false; - sessionInfo = screen.getSessionInfo(); - eventQueue = new LinkedList(); - - screen.frame.addKeyListener(this); - screen.frame.addWindowListener(this); - screen.frame.addComponentListener(this); - screen.frame.addMouseListener(this); - screen.frame.addMouseMotionListener(this); - screen.frame.addMouseWheelListener(this); - } - - /** - * Return any events in the IO queue. - * - * @param queue list to append new events to - */ - public void getEvents(final List queue) { - synchronized (eventQueue) { - if (eventQueue.size() > 0) { - synchronized (queue) { - queue.addAll(eventQueue); - } - eventQueue.clear(); - } - } - } - - /** - * Pass Swing keystrokes into the event queue. - * - * @param key keystroke received - */ - public void keyReleased(final KeyEvent key) { - // Ignore release events - } - - /** - * Pass Swing keystrokes into the event queue. - * - * @param key keystroke received - */ - public void keyTyped(final KeyEvent key) { - // Ignore typed events - } - - /** - * Pass Swing keystrokes into the event queue. - * - * @param key keystroke received - */ - public void keyPressed(final KeyEvent key) { - boolean alt = false; - boolean shift = false; - boolean ctrl = false; - char ch = ' '; - boolean isKey = false; - if (key.isActionKey()) { - isKey = true; - } else { - ch = key.getKeyChar(); - } - alt = key.isAltDown(); - ctrl = key.isControlDown(); - shift = key.isShiftDown(); - - /* - System.err.printf("Swing Key: %s\n", key); - System.err.printf(" isKey: %s\n", isKey); - System.err.printf(" alt: %s\n", alt); - System.err.printf(" ctrl: %s\n", ctrl); - System.err.printf(" shift: %s\n", shift); - System.err.printf(" ch: %s\n", ch); - */ - - // Special case: not return the bare modifier presses - switch (key.getKeyCode()) { - case KeyEvent.VK_ALT: - return; - case KeyEvent.VK_ALT_GRAPH: - return; - case KeyEvent.VK_CONTROL: - return; - case KeyEvent.VK_SHIFT: - return; - case KeyEvent.VK_META: - return; - default: - break; - } - - TKeypress keypress = null; - if (isKey) { - switch (key.getKeyCode()) { - case KeyEvent.VK_F1: - keypress = new TKeypress(true, TKeypress.F1, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_F2: - keypress = new TKeypress(true, TKeypress.F2, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_F3: - keypress = new TKeypress(true, TKeypress.F3, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_F4: - keypress = new TKeypress(true, TKeypress.F4, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_F5: - keypress = new TKeypress(true, TKeypress.F5, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_F6: - keypress = new TKeypress(true, TKeypress.F6, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_F7: - keypress = new TKeypress(true, TKeypress.F7, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_F8: - keypress = new TKeypress(true, TKeypress.F8, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_F9: - keypress = new TKeypress(true, TKeypress.F9, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_F10: - keypress = new TKeypress(true, TKeypress.F10, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_F11: - keypress = new TKeypress(true, TKeypress.F11, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_F12: - keypress = new TKeypress(true, TKeypress.F12, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_HOME: - keypress = new TKeypress(true, TKeypress.HOME, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_END: - keypress = new TKeypress(true, TKeypress.END, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_PAGE_UP: - keypress = new TKeypress(true, TKeypress.PGUP, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_PAGE_DOWN: - keypress = new TKeypress(true, TKeypress.PGDN, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_INSERT: - keypress = new TKeypress(true, TKeypress.INS, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_DELETE: - keypress = new TKeypress(true, TKeypress.DEL, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_RIGHT: - keypress = new TKeypress(true, TKeypress.RIGHT, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_LEFT: - keypress = new TKeypress(true, TKeypress.LEFT, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_UP: - keypress = new TKeypress(true, TKeypress.UP, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_DOWN: - keypress = new TKeypress(true, TKeypress.DOWN, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_TAB: - // Special case: distinguish TAB vs BTAB - if (shift) { - keypress = kbShiftTab; - } else { - keypress = kbTab; - } - break; - case KeyEvent.VK_ENTER: - keypress = new TKeypress(true, TKeypress.ENTER, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_ESCAPE: - keypress = new TKeypress(true, TKeypress.ESC, ' ', - alt, ctrl, shift); - break; - case KeyEvent.VK_BACK_SPACE: - // Special case: return it as kbBackspace (Ctrl-H) - keypress = new TKeypress(false, 0, 'H', false, true, false); - break; - default: - // Unsupported, ignore - return; - } - } - - if (keypress == null) { - switch (ch) { - case 0x08: - keypress = kbBackspace; - break; - case 0x0A: - keypress = kbEnter; - break; - case 0x1B: - keypress = kbEsc; - break; - case 0x0D: - keypress = kbEnter; - break; - case 0x09: - if (shift) { - keypress = kbShiftTab; - } else { - keypress = kbTab; - } - break; - case 0x7F: - keypress = kbDel; - break; - default: - if (!alt && ctrl && !shift) { - ch = KeyEvent.getKeyText(key.getKeyCode()).charAt(0); - } - // Not a special key, put it together - keypress = new TKeypress(false, 0, ch, alt, ctrl, shift); - } - } - - // Save it and we are done. - synchronized (eventQueue) { - eventQueue.add(new TKeypressEvent(keypress)); - } - synchronized (listener) { - listener.notifyAll(); - } - } - - /** - * Pass window events into the event queue. - * - * @param event window event received - */ - public void windowActivated(final WindowEvent event) { - // Force a total repaint - synchronized (screen) { - screen.clearPhysical(); - } - } - - /** - * Pass window events into the event queue. - * - * @param event window event received - */ - public void windowClosed(final WindowEvent event) { - // Ignore - } - - /** - * Pass window events into the event queue. - * - * @param event window event received - */ - public void windowClosing(final WindowEvent event) { - // Drop a cmAbort and walk away - synchronized (eventQueue) { - eventQueue.add(new TCommandEvent(cmAbort)); - } - synchronized (listener) { - listener.notifyAll(); - } - } - - /** - * Pass window events into the event queue. - * - * @param event window event received - */ - public void windowDeactivated(final WindowEvent event) { - // Ignore - } - - /** - * Pass window events into the event queue. - * - * @param event window event received - */ - public void windowDeiconified(final WindowEvent event) { - // Ignore - } - - /** - * Pass window events into the event queue. - * - * @param event window event received - */ - public void windowIconified(final WindowEvent event) { - // Ignore - } - - /** - * Pass window events into the event queue. - * - * @param event window event received - */ - public void windowOpened(final WindowEvent event) { - // Ignore - } - - /** - * Pass component events into the event queue. - * - * @param event component event received - */ - public void componentHidden(final ComponentEvent event) { - // Ignore - } - - /** - * Pass component events into the event queue. - * - * @param event component event received - */ - public void componentShown(final ComponentEvent event) { - // Ignore - } - - /** - * Pass component events into the event queue. - * - * @param event component event received - */ - public void componentMoved(final ComponentEvent event) { - // Ignore - } - - /** - * Pass component events into the event queue. - * - * @param event component event received - */ - public void componentResized(final ComponentEvent event) { - // Drop a new TResizeEvent into the queue - sessionInfo.queryWindowSize(); - synchronized (eventQueue) { - TResizeEvent windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, - sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); - eventQueue.add(windowResize); - } - synchronized (listener) { - listener.notifyAll(); - } - } - - /** - * Pass mouse events into the event queue. - * - * @param mouse mouse event received - */ - public void mouseDragged(final MouseEvent mouse) { - int modifiers = mouse.getModifiersEx(); - boolean eventMouse1 = false; - boolean eventMouse2 = false; - boolean eventMouse3 = false; - if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { - eventMouse1 = true; - } - if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) { - eventMouse2 = true; - } - if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { - eventMouse3 = true; - } - mouse1 = eventMouse1; - mouse2 = eventMouse2; - mouse3 = eventMouse3; - int x = screen.textColumn(mouse.getX()); - int y = screen.textRow(mouse.getY()); - - TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION, - x, y, x, y, mouse1, mouse2, mouse3, false, false); - - synchronized (eventQueue) { - eventQueue.add(mouseEvent); - } - synchronized (listener) { - listener.notifyAll(); - } - } - - /** - * Pass mouse events into the event queue. - * - * @param mouse mouse event received - */ - public void mouseMoved(final MouseEvent mouse) { - int x = screen.textColumn(mouse.getX()); - int y = screen.textRow(mouse.getY()); - if ((x == oldMouseX) && (y == oldMouseY)) { - // Bail out, we've moved some pixels but not a whole text cell. - return; - } - oldMouseX = x; - oldMouseY = y; - - TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION, - x, y, x, y, mouse1, mouse2, mouse3, false, false); - - synchronized (eventQueue) { - eventQueue.add(mouseEvent); - } - synchronized (listener) { - listener.notifyAll(); - } - } - - /** - * Pass mouse events into the event queue. - * - * @param mouse mouse event received - */ - public void mouseClicked(final MouseEvent mouse) { - // Ignore - } - - /** - * Pass mouse events into the event queue. - * - * @param mouse mouse event received - */ - public void mouseEntered(final MouseEvent mouse) { - // Ignore - } - - /** - * Pass mouse events into the event queue. - * - * @param mouse mouse event received - */ - public void mouseExited(final MouseEvent mouse) { - // Ignore - } - - /** - * Pass mouse events into the event queue. - * - * @param mouse mouse event received - */ - public void mousePressed(final MouseEvent mouse) { - int modifiers = mouse.getModifiersEx(); - boolean eventMouse1 = false; - boolean eventMouse2 = false; - boolean eventMouse3 = false; - if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { - eventMouse1 = true; - } - if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) { - eventMouse2 = true; - } - if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { - eventMouse3 = true; - } - mouse1 = eventMouse1; - mouse2 = eventMouse2; - mouse3 = eventMouse3; - int x = screen.textColumn(mouse.getX()); - int y = screen.textRow(mouse.getY()); - - TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN, - x, y, x, y, mouse1, mouse2, mouse3, false, false); - - synchronized (eventQueue) { - eventQueue.add(mouseEvent); - } - synchronized (listener) { - listener.notifyAll(); - } - } - - /** - * Pass mouse events into the event queue. - * - * @param mouse mouse event received - */ - public void mouseReleased(final MouseEvent mouse) { - int modifiers = mouse.getModifiersEx(); - boolean eventMouse1 = false; - boolean eventMouse2 = false; - boolean eventMouse3 = false; - if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { - eventMouse1 = true; - } - if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) { - eventMouse2 = true; - } - if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { - eventMouse3 = true; - } - if (mouse1) { - mouse1 = false; - eventMouse1 = true; - } - if (mouse2) { - mouse2 = false; - eventMouse2 = true; - } - if (mouse3) { - mouse3 = false; - eventMouse3 = true; - } - int x = screen.textColumn(mouse.getX()); - int y = screen.textRow(mouse.getY()); - - TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_UP, - x, y, x, y, eventMouse1, eventMouse2, eventMouse3, false, false); - - synchronized (eventQueue) { - eventQueue.add(mouseEvent); - } - synchronized (listener) { - listener.notifyAll(); - } - } - - /** - * Pass mouse events into the event queue. - * - * @param mouse mouse event received - */ - public void mouseWheelMoved(final MouseWheelEvent mouse) { - int modifiers = mouse.getModifiersEx(); - boolean eventMouse1 = false; - boolean eventMouse2 = false; - boolean eventMouse3 = false; - boolean mouseWheelUp = false; - boolean mouseWheelDown = false; - if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { - eventMouse1 = true; - } - if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) { - eventMouse2 = true; - } - if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { - eventMouse3 = true; - } - mouse1 = eventMouse1; - mouse2 = eventMouse2; - mouse3 = eventMouse3; - int x = screen.textColumn(mouse.getX()); - int y = screen.textRow(mouse.getY()); - if (mouse.getWheelRotation() > 0) { - mouseWheelDown = true; - } - if (mouse.getWheelRotation() < 0) { - mouseWheelUp = true; - } - - TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN, - x, y, x, y, mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown); - - synchronized (eventQueue) { - eventQueue.add(mouseEvent); - } - synchronized (listener) { - listener.notifyAll(); - } - } - -} diff --git a/src/jexer/io/TimeoutInputStream.java b/src/jexer/io/TimeoutInputStream.java index d540c60..db9eb4a 100644 --- a/src/jexer/io/TimeoutInputStream.java +++ b/src/jexer/io/TimeoutInputStream.java @@ -73,7 +73,7 @@ public class TimeoutInputStream extends InputStream { } /** - * Public constructor, using the default 10 bits per byte. + * Public constructor. * * @param stream the wrapped InputStream * @param timeoutMillis the timeout value in millis. If it takes longer diff --git a/src/jexer/io/package-info.java b/src/jexer/io/package-info.java index 04cbc59..4c04cf1 100644 --- a/src/jexer/io/package-info.java +++ b/src/jexer/io/package-info.java @@ -28,6 +28,6 @@ */ /** - * User-facing I/O, including screen, keyboard, and mouse handling classes. + * java.io subclasses. */ package jexer.io; diff --git a/src/jexer/net/TelnetInputStream.java b/src/jexer/net/TelnetInputStream.java index 2a5ed4d..ea30171 100644 --- a/src/jexer/net/TelnetInputStream.java +++ b/src/jexer/net/TelnetInputStream.java @@ -34,7 +34,7 @@ import java.util.ArrayList; import java.util.Map; import java.util.TreeMap; -import jexer.session.SessionInfo; +import jexer.backend.SessionInfo; import static jexer.net.TelnetSocket.*; /**