PMD code sweep, #6 don't add MyWindow twice to MyApplication
authorKevin Lamonte <kevin.lamonte@gmail.com>
Sun, 3 Dec 2017 19:55:39 +0000 (14:55 -0500)
committerKevin Lamonte <kevin.lamonte@gmail.com>
Sun, 3 Dec 2017 19:55:39 +0000 (14:55 -0500)
65 files changed:
README.md
build.xml
docs/worklog.md
resources/jexer_logo_128.png [new file with mode: 0644]
src/jexer/TApplication.java
src/jexer/TButton.java
src/jexer/TCheckbox.java
src/jexer/TCommand.java
src/jexer/TEditColorThemeWindow.java
src/jexer/TField.java
src/jexer/TFileOpenBox.java
src/jexer/THScroller.java
src/jexer/TKeypress.java
src/jexer/TLabel.java
src/jexer/TList.java
src/jexer/TProgressBar.java
src/jexer/TRadioButton.java
src/jexer/TStatusBar.java
src/jexer/TText.java
src/jexer/TTimer.java
src/jexer/TVScroller.java
src/jexer/TWidget.java
src/jexer/TWindow.java
src/jexer/backend/Backend.java
src/jexer/backend/ECMA48Backend.java
src/jexer/backend/ECMA48Terminal.java
src/jexer/backend/GenericBackend.java
src/jexer/backend/LogicalScreen.java
src/jexer/backend/MultiBackend.java
src/jexer/backend/MultiScreen.java
src/jexer/backend/SwingBackend.java
src/jexer/backend/SwingComponent.java
src/jexer/backend/SwingSessionInfo.java
src/jexer/backend/SwingTerminal.java
src/jexer/backend/TSessionInfo.java
src/jexer/backend/TTYSessionInfo.java
src/jexer/backend/TWindowBackend.java
src/jexer/bits/Cell.java
src/jexer/bits/CellAttributes.java
src/jexer/bits/Color.java
src/jexer/bits/ColorTheme.java
src/jexer/bits/GraphicsChars.java
src/jexer/bits/MnemonicString.java
src/jexer/bits/StringUtils.java [moved from src/jexer/bits/StringJustifier.java with 85% similarity]
src/jexer/demos/DemoTreeViewWindow.java
src/jexer/event/TMouseEvent.java
src/jexer/event/TResizeEvent.java
src/jexer/menu/TMenu.java
src/jexer/menu/TMenu.properties
src/jexer/menu/TMenuItem.java
src/jexer/menu/TMenuSeparator.java
src/jexer/menu/TSubMenu.java
src/jexer/net/TelnetInputStream.java
src/jexer/net/TelnetOutputStream.java
src/jexer/net/TelnetServerSocket.java
src/jexer/net/TelnetSocket.java
src/jexer/tterminal/DECCharacterSets.java
src/jexer/tterminal/DisplayLine.java
src/jexer/tterminal/ECMA48.java
src/jexer/ttree/TDirectoryTreeItem.java [moved from src/jexer/TDirectoryTreeItem.java with 80% similarity]
src/jexer/ttree/TTreeItem.java [moved from src/jexer/TTreeItem.java with 76% similarity]
src/jexer/ttree/TTreeView.java [new file with mode: 0644]
src/jexer/ttree/TTreeViewWidget.java [moved from src/jexer/TTreeView.java with 58% similarity]
src/jexer/ttree/TTreeViewWindow.java [new file with mode: 0644]
src/jexer/ttree/package-info.java [new file with mode: 0644]

index 1e2b7e9d4f2f1e7abbbf7efe4c9f64a5ceb6dd90..dc976599bf6cffeb4d37a2d48a3d0bc0932d5d77 100644 (file)
--- a/README.md
+++ b/README.md
@@ -73,8 +73,9 @@ class MyApplication extends TApplication {
         addFileMenu();
         addWindowMenu();
 
-        // Add a custom window, see below for its code.
-        addWindow(new MyWindow(this));
+        // Add a custom window, see below for its code.  The TWindow
+        // constructor will add it to this application.
+        new MyWindow(this);
     }
 
     public static void main(String [] args) {
index f21f321d703367c558c6472ef0533473709da021..4e7abb8a8d0662376fde4e2187879ec3b5d3150b 100644 (file)
--- a/build.xml
+++ b/build.xml
 
 <project name="jexer" basedir="." default="jar">
 
-  <!-- I test with gcj because it is Java 1.5 and has better warnings
-       for unused imports. -->
-  <property name="build.compiler" value="gcj"/>
-
   <property name="version"       value="0.0.6"/>
   <property name="src.dir"       value="src"/>
   <property name="resources.dir" value="resources"/>
@@ -94,7 +90,6 @@
         version="true"
         use="true"
         access="protected"
-        failonwarning="true"
         windowtitle="Jexer - Java Text User Interface - API docs">
 
       <fileset dir="${src.dir}" defaultexcludes="yes">
index f4a3a36d535b530f32c8210b43f3c4b00462d7c3..05d4f15cb79b8948cd055f7835f3c12db0cfcc79 100644 (file)
@@ -1,6 +1,17 @@
 Jexer Work Log
 ==============
 
+October 17, 2017
+
+I finally gave up the ghost on using gcj as the default compiler due
+to its awesome unused imports messages, and learned how to get PMD to
+do that job.  Which promptly created 1000+ warning messages related to
+class item order (variables, constructors, methods), nested ifs,
+useless checks, and so on.  So now we go on a code sweep to fix those,
+and along the way set a new class template.  Since this is so large
+and invasive, I will bite the bullet now and get it done before the
+next release which will get it out on Maven finally.
+
 August 16, 2017
 
 Holy balls this has gotten so much faster!  It is FINALLY visibly
diff --git a/resources/jexer_logo_128.png b/resources/jexer_logo_128.png
new file mode 100644 (file)
index 0000000..5c3a813
Binary files /dev/null and b/resources/jexer_logo_128.png differ
index e61cea28d631afba3c84964c7a2a13803c597aca..6dd503f547a141d1a05f0511652c14e038a25f42 100644 (file)
@@ -77,7 +77,7 @@ public class TApplication implements Runnable {
     private static final ResourceBundle i18n = ResourceBundle.getBundle(TApplication.class.getName());
 
     // ------------------------------------------------------------------------
-    // Public constants -------------------------------------------------------
+    // Constants --------------------------------------------------------------
     // ------------------------------------------------------------------------
 
     /**
@@ -117,9 +117,155 @@ public class TApplication implements Runnable {
     }
 
     // ------------------------------------------------------------------------
-    // Primary/secondary event handlers ---------------------------------------
+    // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * The primary event handler thread.
+     */
+    private volatile WidgetEventHandler primaryEventHandler;
+
+    /**
+     * The secondary event handler thread.
+     */
+    private volatile WidgetEventHandler secondaryEventHandler;
+
+    /**
+     * The widget receiving events from the secondary event handler thread.
+     */
+    private volatile TWidget secondaryEventReceiver;
+
+    /**
+     * Access to the physical screen, keyboard, and mouse.
+     */
+    private Backend backend;
+
+    /**
+     * Actual mouse coordinate X.
+     */
+    private int mouseX;
+
+    /**
+     * Actual mouse coordinate Y.
+     */
+    private int mouseY;
+
+    /**
+     * Old version of mouse coordinate X.
+     */
+    private int oldMouseX;
+
+    /**
+     * Old version mouse coordinate Y.
+     */
+    private int oldMouseY;
+
+    /**
+     * The last mouse up click time, used to determine if this is a mouse
+     * double-click.
+     */
+    private long lastMouseUpTime;
+
+    /**
+     * The amount of millis between mouse up events to assume a double-click.
+     */
+    private long doubleClickTime = 250;
+
+    /**
+     * Event queue that is filled by run().
+     */
+    private List<TInputEvent> fillEventQueue;
+
+    /**
+     * Event queue that will be drained by either primary or secondary
+     * Thread.
+     */
+    private List<TInputEvent> drainEventQueue;
+
+    /**
+     * Top-level menus in this application.
+     */
+    private List<TMenu> menus;
+
+    /**
+     * Stack of activated sub-menus in this application.
+     */
+    private List<TMenu> subMenus;
+
+    /**
+     * The currently active menu.
+     */
+    private TMenu activeMenu = null;
+
+    /**
+     * Active keyboard accelerators.
+     */
+    private Map<TKeypress, TMenuItem> accelerators;
+
+    /**
+     * All menu items.
+     */
+    private List<TMenuItem> menuItems;
+
+    /**
+     * Windows and widgets pull colors from this ColorTheme.
+     */
+    private ColorTheme theme;
+
+    /**
+     * The top-level windows (but not menus).
+     */
+    private List<TWindow> windows;
+
+    /**
+     * The currently acive window.
+     */
+    private TWindow activeWindow = null;
+
+    /**
+     * Timers that are being ticked.
+     */
+    private List<TTimer> timers;
+
+    /**
+     * When true, the application has been started.
+     */
+    private volatile boolean started = false;
+
+    /**
+     * When true, exit the application.
+     */
+    private volatile boolean quit = false;
+
+    /**
+     * When true, repaint the entire screen.
+     */
+    private volatile boolean repaint = true;
+
+    /**
+     * Y coordinate of the top edge of the desktop.  For now this is a
+     * constant.  Someday it would be nice to have a multi-line menu or
+     * toolbars.
+     */
+    private static final int desktopTop = 1;
+
+    /**
+     * Y coordinate of the bottom edge of the desktop.
+     */
+    private int desktopBottom;
+
+    /**
+     * An optional TDesktop background window that is drawn underneath
+     * everything else.
+     */
+    private TDesktop desktop;
+
+    /**
+     * If true, focus follows mouse: windows automatically raised if the
+     * mouse passes over them.
+     */
+    private boolean focusFollowsMouse = false;
+
     /**
      * WidgetEventHandler is the main event consumer loop.  There are at most
      * two such threads in existence: the primary for normal case and a
@@ -264,1163 +410,1153 @@ public class TApplication implements Runnable {
         }
     }
 
-    /**
-     * The primary event handler thread.
-     */
-    private volatile WidgetEventHandler primaryEventHandler;
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
-     * The secondary event handler thread.
+     * Public constructor.
+     *
+     * @param backendType BackendType.XTERM, BackendType.ECMA48 or
+     * BackendType.SWING
+     * @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
+     * @throws UnsupportedEncodingException if an exception is thrown when
+     * creating the InputStreamReader
      */
-    private volatile WidgetEventHandler secondaryEventHandler;
+    public TApplication(final BackendType backendType, final int windowWidth,
+        final int windowHeight, final int fontSize)
+        throws UnsupportedEncodingException {
 
-    /**
-     * The widget receiving events from the secondary event handler thread.
-     */
-    private volatile TWidget secondaryEventReceiver;
+        switch (backendType) {
+        case SWING:
+            backend = new SwingBackend(this, windowWidth, windowHeight,
+                fontSize);
+            break;
+        case XTERM:
+            // Fall through...
+        case ECMA48:
+            backend = new ECMA48Backend(this, null, null, windowWidth,
+                windowHeight, fontSize);
+            break;
+        default:
+            throw new IllegalArgumentException("Invalid backend type: "
+                + backendType);
+        }
+        TApplicationImpl();
+    }
 
     /**
-     * Wake the sleeping active event handler.
+     * Public constructor.
+     *
+     * @param backendType BackendType.XTERM, BackendType.ECMA48 or
+     * BackendType.SWING
+     * @throws UnsupportedEncodingException if an exception is thrown when
+     * creating the InputStreamReader
      */
-    private void wakeEventHandler() {
-        if (!started) {
-            return;
-        }
+    public TApplication(final BackendType backendType)
+        throws UnsupportedEncodingException {
 
-        if (secondaryEventHandler != null) {
-            synchronized (secondaryEventHandler) {
-                secondaryEventHandler.notify();
-            }
-        } else {
-            assert (primaryEventHandler != null);
-            synchronized (primaryEventHandler) {
-                primaryEventHandler.notify();
-            }
+        switch (backendType) {
+        case SWING:
+            // The default SwingBackend is 80x25, 20 pt font.  If you want to
+            // change that, you can pass the extra arguments to the
+            // SwingBackend constructor here.  For example, if you wanted
+            // 90x30, 16 pt font:
+            //
+            // backend = new SwingBackend(this, 90, 30, 16);
+            backend = new SwingBackend(this);
+            break;
+        case XTERM:
+            // Fall through...
+        case ECMA48:
+            backend = new ECMA48Backend(this, null, null);
+            break;
+        default:
+            throw new IllegalArgumentException("Invalid backend type: "
+                + backendType);
         }
+        TApplicationImpl();
     }
 
-    // ------------------------------------------------------------------------
-    // TApplication attributes ------------------------------------------------
-    // ------------------------------------------------------------------------
-
     /**
-     * Access to the physical screen, keyboard, and mouse.
-     */
-    private Backend backend;
-
-    /**
-     * Get the Backend.
+     * Public constructor.  The backend type will be BackendType.ECMA48.
      *
-     * @return the Backend
+     * @param input an InputStream connected to the remote user, or null for
+     * System.in.  If System.in is used, then on non-Windows systems it will
+     * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
+     * mode.  input is always converted to a Reader with UTF-8 encoding.
+     * @param output an OutputStream connected to the remote user, or null
+     * for System.out.  output is always converted to a Writer with UTF-8
+     * encoding.
+     * @throws UnsupportedEncodingException if an exception is thrown when
+     * creating the InputStreamReader
      */
-    public final Backend getBackend() {
-        return backend;
+    public TApplication(final InputStream input,
+        final OutputStream output) throws UnsupportedEncodingException {
+
+        backend = new ECMA48Backend(this, input, output);
+        TApplicationImpl();
     }
 
     /**
-     * Get the Screen.
+     * Public constructor.  The backend type will be BackendType.ECMA48.
      *
-     * @return the Screen
+     * @param input the InputStream underlying 'reader'.  Its available()
+     * method is used to determine if reader.read() will block or not.
+     * @param reader a Reader connected to the remote user.
+     * @param writer a PrintWriter connected to the remote user.
+     * @param setRawMode if true, set System.in into raw mode with stty.
+     * This should in general not be used.  It is here solely for Demo3,
+     * which uses System.in.
+     * @throws IllegalArgumentException if input, reader, or writer are null.
      */
-    public final Screen getScreen() {
-        if (backend instanceof TWindowBackend) {
-            // We are being rendered to a TWindow.  We can't use its
-            // getScreen() method because that is how it is rendering to a
-            // hardware backend somewhere.  Instead use its getOtherScreen()
-            // method.
-            return ((TWindowBackend) backend).getOtherScreen();
-        } else {
-            return backend.getScreen();
-        }
+    public TApplication(final InputStream input, final Reader reader,
+        final PrintWriter writer, final boolean setRawMode) {
+
+        backend = new ECMA48Backend(this, input, reader, writer, setRawMode);
+        TApplicationImpl();
     }
 
     /**
-     * Actual mouse coordinate X.
+     * Public constructor.  The backend type will be BackendType.ECMA48.
+     *
+     * @param input the InputStream underlying 'reader'.  Its available()
+     * method is used to determine if reader.read() will block or not.
+     * @param reader a Reader connected to the remote user.
+     * @param writer a PrintWriter connected to the remote user.
+     * @throws IllegalArgumentException if input, reader, or writer are null.
      */
-    private int mouseX;
+    public TApplication(final InputStream input, final Reader reader,
+        final PrintWriter writer) {
 
-    /**
-     * Actual mouse coordinate Y.
-     */
-    private int mouseY;
+        this(input, reader, writer, false);
+    }
 
     /**
-     * Old version of mouse coordinate X.
+     * Public constructor.  This hook enables use with new non-Jexer
+     * backends.
+     *
+     * @param backend a Backend that is already ready to go.
      */
-    private int oldMouseX;
+    public TApplication(final Backend backend) {
+        this.backend = backend;
+        backend.setListener(this);
+        TApplicationImpl();
+    }
 
     /**
-     * Old version mouse coordinate Y.
+     * Finish construction once the backend is set.
      */
-    private int oldMouseY;
+    private void TApplicationImpl() {
+        theme           = new ColorTheme();
+        desktopBottom   = getScreen().getHeight() - 1;
+        fillEventQueue  = new ArrayList<TInputEvent>();
+        drainEventQueue = new ArrayList<TInputEvent>();
+        windows         = new LinkedList<TWindow>();
+        menus           = new LinkedList<TMenu>();
+        subMenus        = new LinkedList<TMenu>();
+        timers          = new LinkedList<TTimer>();
+        accelerators    = new HashMap<TKeypress, TMenuItem>();
+        menuItems       = new ArrayList<TMenuItem>();
+        desktop         = new TDesktop(this);
 
-    /**
-     * The last mouse up click time, used to determine if this is a mouse
-     * double-click.
-     */
-    private long lastMouseUpTime;
+        // Special case: the Swing backend needs to have a timer to drive its
+        // blink state.
+        if ((backend instanceof SwingBackend)
+            || (backend instanceof MultiBackend)
+        ) {
+            // Default to 500 millis, unless a SwingBackend has its own
+            // value.
+            long millis = 500;
+            if (backend instanceof SwingBackend) {
+                millis = ((SwingBackend) backend).getBlinkMillis();
+            }
+            if (millis > 0) {
+                addTimer(millis, true,
+                    new TAction() {
+                        public void DO() {
+                            TApplication.this.doRepaint();
+                        }
+                    }
+                );
+            }
+        }
+    }
 
-    /**
-     * The amount of millis between mouse up events to assume a double-click.
-     */
-    private long doubleClickTime = 250;
+    // ------------------------------------------------------------------------
+    // Runnable ---------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
-     * Event queue that is filled by run().
+     * Run this application until it exits.
      */
-    private List<TInputEvent> fillEventQueue;
+    public void run() {
+        // Start the main consumer thread
+        primaryEventHandler = new WidgetEventHandler(this, true);
+        (new Thread(primaryEventHandler)).start();
 
-    /**
-     * Event queue that will be drained by either primary or secondary
-     * Thread.
-     */
-    private List<TInputEvent> drainEventQueue;
+        started = true;
 
-    /**
-     * Top-level menus in this application.
-     */
-    private List<TMenu> menus;
+        while (!quit) {
+            synchronized (this) {
+                boolean doWait = false;
 
-    /**
-     * Stack of activated sub-menus in this application.
-     */
-    private List<TMenu> subMenus;
+                if (!backend.hasEvents()) {
+                    synchronized (fillEventQueue) {
+                        if (fillEventQueue.size() == 0) {
+                            doWait = true;
+                        }
+                    }
+                }
 
-    /**
-     * The currently active menu.
-     */
-    private TMenu activeMenu = null;
+                if (doWait) {
+                    // No I/O to dispatch, so wait until the backend
+                    // provides new I/O.
+                    try {
+                        if (debugThreads) {
+                            System.err.println(System.currentTimeMillis() +
+                                " MAIN sleep");
+                        }
 
-    /**
-     * Active keyboard accelerators.
-     */
-    private Map<TKeypress, TMenuItem> accelerators;
+                        this.wait();
 
-    /**
-     * All menu items.
-     */
-    private List<TMenuItem> menuItems;
+                        if (debugThreads) {
+                            System.err.println(System.currentTimeMillis() +
+                                " MAIN AWAKE");
+                        }
+                    } catch (InterruptedException e) {
+                        // I'm awake and don't care why, let's see what's
+                        // going on out there.
+                    }
+                }
 
-    /**
-     * Windows and widgets pull colors from this ColorTheme.
-     */
-    private ColorTheme theme;
+            } // synchronized (this)
 
-    /**
-     * Get the color theme.
-     *
-     * @return the theme
-     */
-    public final ColorTheme getTheme() {
-        return theme;
-    }
+            synchronized (fillEventQueue) {
+                // Pull any pending I/O events
+                backend.getEvents(fillEventQueue);
 
-    /**
-     * The top-level windows (but not menus).
-     */
-    private List<TWindow> windows;
+                // Dispatch each event to the appropriate handler, one at a
+                // time.
+                for (;;) {
+                    TInputEvent event = null;
+                    if (fillEventQueue.size() == 0) {
+                        break;
+                    }
+                    event = fillEventQueue.remove(0);
+                    metaHandleEvent(event);
+                }
+            }
 
-    /**
-     * The currently acive window.
-     */
-    private TWindow activeWindow = null;
+            // Wake a consumer thread if we have any pending events.
+            if (drainEventQueue.size() > 0) {
+                wakeEventHandler();
+            }
 
-    /**
-     * Timers that are being ticked.
-     */
-    private List<TTimer> timers;
+        } // while (!quit)
 
-    /**
-     * When true, the application has been started.
-     */
-    private volatile boolean started = false;
+        // Shutdown the event consumer threads
+        if (secondaryEventHandler != null) {
+            synchronized (secondaryEventHandler) {
+                secondaryEventHandler.notify();
+            }
+        }
+        if (primaryEventHandler != null) {
+            synchronized (primaryEventHandler) {
+                primaryEventHandler.notify();
+            }
+        }
 
-    /**
-     * When true, exit the application.
-     */
-    private volatile boolean quit = false;
+        // Shutdown the user I/O thread(s)
+        backend.shutdown();
 
-    /**
-     * When true, repaint the entire screen.
-     */
-    private volatile boolean repaint = true;
+        // Close all the windows.  This gives them an opportunity to release
+        // resources.
+        closeAllWindows();
 
-    /**
-     * Repaint the screen on the next update.
-     */
-    public void doRepaint() {
-        repaint = true;
-        wakeEventHandler();
     }
 
-    /**
-     * Y coordinate of the top edge of the desktop.  For now this is a
-     * constant.  Someday it would be nice to have a multi-line menu or
-     * toolbars.
-     */
-    private static final int desktopTop = 1;
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
-     * Get Y coordinate of the top edge of the desktop.
+     * Method that TApplication subclasses can override to handle menu or
+     * posted command events.
      *
-     * @return Y coordinate of the top edge of the desktop
+     * @param command command event
+     * @return if true, this event was consumed
      */
-    public final int getDesktopTop() {
-        return desktopTop;
-    }
-
-    /**
-     * Y coordinate of the bottom edge of the desktop.
-     */
-    private int desktopBottom;
+    protected boolean onCommand(final TCommandEvent command) {
+        // Default: handle cmExit
+        if (command.equals(cmExit)) {
+            if (messageBox(i18n.getString("exitDialogTitle"),
+                    i18n.getString("exitDialogText"),
+                    TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
+                exit();
+            }
+            return true;
+        }
 
-    /**
-     * Get Y coordinate of the bottom edge of the desktop.
-     *
-     * @return Y coordinate of the bottom edge of the desktop
-     */
-    public final int getDesktopBottom() {
-        return desktopBottom;
-    }
+        if (command.equals(cmShell)) {
+            openTerminal(0, 0, TWindow.RESIZABLE);
+            return true;
+        }
 
-    /**
-     * An optional TDesktop background window that is drawn underneath
-     * everything else.
-     */
-    private TDesktop desktop;
+        if (command.equals(cmTile)) {
+            tileWindows();
+            return true;
+        }
+        if (command.equals(cmCascade)) {
+            cascadeWindows();
+            return true;
+        }
+        if (command.equals(cmCloseAll)) {
+            closeAllWindows();
+            return true;
+        }
 
-    /**
-     * Set the TDesktop instance.
-     *
-     * @param desktop a TDesktop instance, or null to remove the one that is
-     * set
-     */
-    public final void setDesktop(final TDesktop desktop) {
-        if (this.desktop != null) {
-            this.desktop.onClose();
+        if (command.equals(cmMenu)) {
+            if (!modalWindowActive() && (activeMenu == null)) {
+                if (menus.size() > 0) {
+                    menus.get(0).setActive(true);
+                    activeMenu = menus.get(0);
+                    return true;
+                }
+            }
         }
-        this.desktop = desktop;
-    }
 
-    /**
-     * Get the TDesktop instance.
-     *
-     * @return the desktop, or null if it is not set
-     */
-    public final TDesktop getDesktop() {
-        return desktop;
+        return false;
     }
 
     /**
-     * Get the current active window.
+     * Method that TApplication subclasses can override to handle menu
+     * events.
      *
-     * @return the active window, or null if it is not set
+     * @param menu menu event
+     * @return if true, this event was consumed
      */
-    public final TWindow getActiveWindow() {
-        return activeWindow;
-    }
+    protected boolean onMenu(final TMenuEvent menu) {
 
-    /**
-     * Get a (shallow) copy of the window list.
-     *
-     * @return a copy of the list of windows for this application
-     */
-    public final List<TWindow> getAllWindows() {
-        List<TWindow> result = new LinkedList<TWindow>();
-        result.addAll(windows);
-        return result;
-    }
+        // Default: handle MID_EXIT
+        if (menu.getId() == TMenu.MID_EXIT) {
+            if (messageBox(i18n.getString("exitDialogTitle"),
+                    i18n.getString("exitDialogText"),
+                    TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
+                exit();
+            }
+            return true;
+        }
 
-    /**
-     * If true, focus follows mouse: windows automatically raised if the
-     * mouse passes over them.
-     */
-    private boolean focusFollowsMouse = false;
+        if (menu.getId() == TMenu.MID_SHELL) {
+            openTerminal(0, 0, TWindow.RESIZABLE);
+            return true;
+        }
 
-    /**
-     * Get focusFollowsMouse flag.
-     *
-     * @return true if focus follows mouse: windows automatically raised if
-     * the mouse passes over them
-     */
-    public boolean getFocusFollowsMouse() {
-        return focusFollowsMouse;
+        if (menu.getId() == TMenu.MID_TILE) {
+            tileWindows();
+            return true;
+        }
+        if (menu.getId() == TMenu.MID_CASCADE) {
+            cascadeWindows();
+            return true;
+        }
+        if (menu.getId() == TMenu.MID_CLOSE_ALL) {
+            closeAllWindows();
+            return true;
+        }
+        if (menu.getId() == TMenu.MID_ABOUT) {
+            showAboutDialog();
+            return true;
+        }
+        if (menu.getId() == TMenu.MID_REPAINT) {
+            doRepaint();
+            return true;
+        }
+        return false;
     }
 
     /**
-     * Set focusFollowsMouse flag.
+     * Method that TApplication subclasses can override to handle keystrokes.
      *
-     * @param focusFollowsMouse if true, focus follows mouse: windows
-     * automatically raised if the mouse passes over them
+     * @param keypress keystroke event
+     * @return if true, this event was consumed
      */
-    public void setFocusFollowsMouse(final boolean focusFollowsMouse) {
-        this.focusFollowsMouse = focusFollowsMouse;
-    }
+    protected boolean onKeypress(final TKeypressEvent keypress) {
+        // Default: only menu shortcuts
 
-    // ------------------------------------------------------------------------
-    // General behavior -------------------------------------------------------
-    // ------------------------------------------------------------------------
+        // Process Alt-F, Alt-E, etc. menu shortcut keys
+        if (!keypress.getKey().isFnKey()
+            && keypress.getKey().isAlt()
+            && !keypress.getKey().isCtrl()
+            && (activeMenu == null)
+            && !modalWindowActive()
+        ) {
 
-    /**
-     * Display the about dialog.
-     */
-    protected void showAboutDialog() {
-        messageBox(i18n.getString("aboutDialogTitle"),
-            MessageFormat.format(i18n.getString("aboutDialogText"),
-                this.getClass().getPackage().getImplementationVersion()),
-            TMessageBox.Type.OK);
-    }
+            assert (subMenus.size() == 0);
 
-    // ------------------------------------------------------------------------
-    // Constructors -----------------------------------------------------------
-    // ------------------------------------------------------------------------
+            for (TMenu menu: menus) {
+                if (Character.toLowerCase(menu.getMnemonic().getShortcut())
+                    == Character.toLowerCase(keypress.getKey().getChar())
+                ) {
+                    activeMenu = menu;
+                    menu.setActive(true);
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
 
     /**
-     * Public constructor.
-     *
-     * @param backendType BackendType.XTERM, BackendType.ECMA48 or
-     * BackendType.SWING
-     * @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
-     * @throws UnsupportedEncodingException if an exception is thrown when
-     * creating the InputStreamReader
+     * Process background events, and update the screen.
      */
-    public TApplication(final BackendType backendType, final int windowWidth,
-        final int windowHeight, final int fontSize)
-        throws UnsupportedEncodingException {
+    private void finishEventProcessing() {
+        if (debugThreads) {
+            System.err.printf(System.currentTimeMillis() + " " +
+                Thread.currentThread() + " finishEventProcessing()\n");
+        }
 
-        switch (backendType) {
-        case SWING:
-            backend = new SwingBackend(this, windowWidth, windowHeight,
-                fontSize);
-            break;
-        case XTERM:
-            // Fall through...
-        case ECMA48:
-            backend = new ECMA48Backend(this, null, null, windowWidth,
-                windowHeight, fontSize);
-            break;
-        default:
-            throw new IllegalArgumentException("Invalid backend type: "
-                + backendType);
+        // Process timers and call doIdle()'s
+        doIdle();
+
+        // Update the screen
+        synchronized (getScreen()) {
+            drawAll();
+        }
+
+        if (debugThreads) {
+            System.err.printf(System.currentTimeMillis() + " " +
+                Thread.currentThread() + " finishEventProcessing() END\n");
         }
-        TApplicationImpl();
     }
 
     /**
-     * Public constructor.
+     * Peek at certain application-level events, add to eventQueue, and wake
+     * up the consuming Thread.
      *
-     * @param backendType BackendType.XTERM, BackendType.ECMA48 or
-     * BackendType.SWING
-     * @throws UnsupportedEncodingException if an exception is thrown when
-     * creating the InputStreamReader
+     * @param event the input event to consume
      */
-    public TApplication(final BackendType backendType)
-        throws UnsupportedEncodingException {
+    private void metaHandleEvent(final TInputEvent event) {
 
-        switch (backendType) {
-        case SWING:
-            // The default SwingBackend is 80x25, 20 pt font.  If you want to
-            // change that, you can pass the extra arguments to the
-            // SwingBackend constructor here.  For example, if you wanted
-            // 90x30, 16 pt font:
-            //
-            // backend = new SwingBackend(this, 90, 30, 16);
-            backend = new SwingBackend(this);
-            break;
-        case XTERM:
-            // Fall through...
-        case ECMA48:
-            backend = new ECMA48Backend(this, null, null);
-            break;
-        default:
-            throw new IllegalArgumentException("Invalid backend type: "
-                + backendType);
+        if (debugEvents) {
+            System.err.printf(String.format("metaHandleEvents event: %s\n",
+                    event)); System.err.flush();
         }
-        TApplicationImpl();
-    }
-
-    /**
-     * Public constructor.  The backend type will be BackendType.ECMA48.
-     *
-     * @param input an InputStream connected to the remote user, or null for
-     * System.in.  If System.in is used, then on non-Windows systems it will
-     * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
-     * mode.  input is always converted to a Reader with UTF-8 encoding.
-     * @param output an OutputStream connected to the remote user, or null
-     * for System.out.  output is always converted to a Writer with UTF-8
-     * encoding.
-     * @throws UnsupportedEncodingException if an exception is thrown when
-     * creating the InputStreamReader
-     */
-    public TApplication(final InputStream input,
-        final OutputStream output) throws UnsupportedEncodingException {
-
-        backend = new ECMA48Backend(this, input, output);
-        TApplicationImpl();
-    }
-
-    /**
-     * Public constructor.  The backend type will be BackendType.ECMA48.
-     *
-     * @param input the InputStream underlying 'reader'.  Its available()
-     * method is used to determine if reader.read() will block or not.
-     * @param reader a Reader connected to the remote user.
-     * @param writer a PrintWriter connected to the remote user.
-     * @param setRawMode if true, set System.in into raw mode with stty.
-     * This should in general not be used.  It is here solely for Demo3,
-     * which uses System.in.
-     * @throws IllegalArgumentException if input, reader, or writer are null.
-     */
-    public TApplication(final InputStream input, final Reader reader,
-        final PrintWriter writer, final boolean setRawMode) {
-
-        backend = new ECMA48Backend(this, input, reader, writer, setRawMode);
-        TApplicationImpl();
-    }
-
-    /**
-     * Public constructor.  The backend type will be BackendType.ECMA48.
-     *
-     * @param input the InputStream underlying 'reader'.  Its available()
-     * method is used to determine if reader.read() will block or not.
-     * @param reader a Reader connected to the remote user.
-     * @param writer a PrintWriter connected to the remote user.
-     * @throws IllegalArgumentException if input, reader, or writer are null.
-     */
-    public TApplication(final InputStream input, final Reader reader,
-        final PrintWriter writer) {
 
-        this(input, reader, writer, false);
-    }
-
-    /**
-     * Public constructor.  This hook enables use with new non-Jexer
-     * backends.
-     *
-     * @param backend a Backend that is already ready to go.
-     */
-    public TApplication(final Backend backend) {
-        this.backend = backend;
-        backend.setListener(this);
-        TApplicationImpl();
-    }
+        if (quit) {
+            // Do no more processing if the application is already trying
+            // to exit.
+            return;
+        }
 
-    /**
-     * Finish construction once the backend is set.
-     */
-    private void TApplicationImpl() {
-        theme           = new ColorTheme();
-        desktopBottom   = getScreen().getHeight() - 1;
-        fillEventQueue  = new ArrayList<TInputEvent>();
-        drainEventQueue = new ArrayList<TInputEvent>();
-        windows         = new LinkedList<TWindow>();
-        menus           = new LinkedList<TMenu>();
-        subMenus        = new LinkedList<TMenu>();
-        timers          = new LinkedList<TTimer>();
-        accelerators    = new HashMap<TKeypress, TMenuItem>();
-        menuItems       = new ArrayList<TMenuItem>();
-        desktop         = new TDesktop(this);
+        // Special application-wide events -------------------------------
 
-        // Special case: the Swing backend needs to have a timer to drive its
-        // blink state.
-        if ((backend instanceof SwingBackend)
-            || (backend instanceof MultiBackend)
-        ) {
-            // Default to 500 millis, unless a SwingBackend has its own
-            // value.
-            long millis = 500;
-            if (backend instanceof SwingBackend) {
-                millis = ((SwingBackend) backend).getBlinkMillis();
-            }
-            if (millis > 0) {
-                addTimer(millis, true,
-                    new TAction() {
-                        public void DO() {
-                            TApplication.this.doRepaint();
-                        }
-                    }
-                );
+        // Abort everything
+        if (event instanceof TCommandEvent) {
+            TCommandEvent command = (TCommandEvent) event;
+            if (command.getCmd().equals(cmAbort)) {
+                exit();
+                return;
             }
         }
-    }
 
-    // ------------------------------------------------------------------------
-    // Screen refresh loop ----------------------------------------------------
-    // ------------------------------------------------------------------------
+        synchronized (drainEventQueue) {
+            // Screen resize
+            if (event instanceof TResizeEvent) {
+                TResizeEvent resize = (TResizeEvent) event;
+                synchronized (getScreen()) {
+                    getScreen().setDimensions(resize.getWidth(),
+                        resize.getHeight());
+                    desktopBottom = getScreen().getHeight() - 1;
+                    mouseX = 0;
+                    mouseY = 0;
+                    oldMouseX = 0;
+                    oldMouseY = 0;
+                }
+                if (desktop != null) {
+                    desktop.setDimensions(0, 0, resize.getWidth(),
+                        resize.getHeight() - 1);
+                }
 
-    /**
-     * Process background events, and update the screen.
-     */
-    private void finishEventProcessing() {
-        if (debugThreads) {
-            System.err.printf(System.currentTimeMillis() + " " +
-                Thread.currentThread() + " finishEventProcessing()\n");
-        }
+                // Change menu edges if needed.
+                recomputeMenuX();
 
-        // Process timers and call doIdle()'s
-        doIdle();
+                // We are dirty, redraw the screen.
+                doRepaint();
 
-        // Update the screen
-        synchronized (getScreen()) {
-            drawAll();
-        }
+                /*
+                System.err.println("New screen: " + resize.getWidth() +
+                    " x " + resize.getHeight());
+                */
+                return;
+            }
 
-        if (debugThreads) {
-            System.err.printf(System.currentTimeMillis() + " " +
-                Thread.currentThread() + " finishEventProcessing() END\n");
+            // Put into the main queue
+            drainEventQueue.add(event);
         }
     }
 
     /**
-     * Invert the cell color at a position.  This is used to track the mouse.
+     * Dispatch one event to the appropriate widget or application-level
+     * event handler.  This is the primary event handler, it has the normal
+     * application-wide event handling.
      *
-     * @param x column position
-     * @param y row position
+     * @param event the input event to consume
+     * @see #secondaryHandleEvent(TInputEvent event)
      */
-    private void invertCell(final int x, final int y) {
-        if (debugThreads) {
-            System.err.printf("%d %s invertCell() %d %d\n",
-                System.currentTimeMillis(), Thread.currentThread(), x, y);
+    private void primaryHandleEvent(final TInputEvent event) {
+
+        if (debugEvents) {
+            System.err.printf("Handle event: %s\n", event);
         }
-        CellAttributes attr = getScreen().getAttrXY(x, y);
-        attr.setForeColor(attr.getForeColor().invert());
-        attr.setBackColor(attr.getBackColor().invert());
-        getScreen().putAttrXY(x, y, attr, false);
-    }
+        TMouseEvent doubleClick = null;
 
-    /**
-     * Draw everything.
-     */
-    private void drawAll() {
-        boolean menuIsActive = false;
+        // Special application-wide events -----------------------------------
 
-        if (debugThreads) {
-            System.err.printf("%d %s drawAll() enter\n",
-                System.currentTimeMillis(), Thread.currentThread());
-        }
+        // Peek at the mouse position
+        if (event instanceof TMouseEvent) {
+            TMouseEvent mouse = (TMouseEvent) event;
+            if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
+                oldMouseX = mouseX;
+                oldMouseY = mouseY;
+                mouseX = mouse.getX();
+                mouseY = mouse.getY();
+            } else {
+                if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
+                    if ((mouse.getTime().getTime() - lastMouseUpTime) <
+                        doubleClickTime) {
 
-        if (!repaint) {
-            if (debugThreads) {
-                System.err.printf("%d %s drawAll() !repaint\n",
-                    System.currentTimeMillis(), Thread.currentThread());
-            }
-            synchronized (getScreen()) {
-                if ((oldMouseX != mouseX) || (oldMouseY != mouseY)) {
-                    // The only thing that has happened is the mouse moved.
-                    // Clear the old position and draw the new position.
-                    invertCell(oldMouseX, oldMouseY);
-                    invertCell(mouseX, mouseY);
-                    oldMouseX = mouseX;
-                    oldMouseY = mouseY;
-                }
-                if (getScreen().isDirty()) {
-                    backend.flushScreen();
+                        // This is a double-click.
+                        doubleClick = new TMouseEvent(TMouseEvent.Type.
+                            MOUSE_DOUBLE_CLICK,
+                            mouse.getX(), mouse.getY(),
+                            mouse.getAbsoluteX(), mouse.getAbsoluteY(),
+                            mouse.isMouse1(), mouse.isMouse2(),
+                            mouse.isMouse3(),
+                            mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+
+                    } else {
+                        // The first click of a potential double-click.
+                        lastMouseUpTime = mouse.getTime().getTime();
+                    }
                 }
-                return;
             }
-        }
 
-        if (debugThreads) {
-            System.err.printf("%d %s drawAll() REDRAW\n",
-                System.currentTimeMillis(), Thread.currentThread());
+            // See if we need to switch focus to another window or the menu
+            checkSwitchFocus((TMouseEvent) event);
         }
 
-        // If true, the cursor is not visible
-        boolean cursor = false;
-
-        // Start with a clean screen
-        getScreen().clear();
+        // Handle menu events
+        if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
+            TMenu menu = activeMenu;
 
-        // Draw the desktop
-        if (desktop != null) {
-            desktop.drawChildren();
-        }
+            if (event instanceof TMouseEvent) {
+                TMouseEvent mouse = (TMouseEvent) event;
 
-        // Draw each window in reverse Z order
-        List<TWindow> sorted = new LinkedList<TWindow>(windows);
-        Collections.sort(sorted);
-        TWindow topLevel = null;
-        if (sorted.size() > 0) {
-            topLevel = sorted.get(0);
-        }
-        Collections.reverse(sorted);
-        for (TWindow window: sorted) {
-            if (window.isShown()) {
-                window.drawChildren();
-            }
-        }
-
-        // Draw the blank menubar line - reset the screen clipping first so
-        // it won't trim it out.
-        getScreen().resetClipping();
-        getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
-            theme.getColor("tmenu"));
-        // Now draw the menus.
-        int x = 1;
-        for (TMenu menu: menus) {
-            CellAttributes menuColor;
-            CellAttributes menuMnemonicColor;
-            if (menu.isActive()) {
-                menuIsActive = true;
-                menuColor = theme.getColor("tmenu.highlighted");
-                menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
-                topLevel = menu;
-            } else {
-                menuColor = theme.getColor("tmenu");
-                menuMnemonicColor = theme.getColor("tmenu.mnemonic");
-            }
-            // Draw the menu title
-            getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ',
-                menuColor);
-            getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor);
-            // Draw the highlight character
-            getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(),
-                0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
+                while (subMenus.size() > 0) {
+                    TMenu subMenu = subMenus.get(subMenus.size() - 1);
+                    if (subMenu.mouseWouldHit(mouse)) {
+                        break;
+                    }
+                    if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
+                        && (!mouse.isMouse1())
+                        && (!mouse.isMouse2())
+                        && (!mouse.isMouse3())
+                        && (!mouse.isMouseWheelUp())
+                        && (!mouse.isMouseWheelDown())
+                    ) {
+                        break;
+                    }
+                    // We navigated away from a sub-menu, so close it
+                    closeSubMenu();
+                }
 
-            if (menu.isActive()) {
-                menu.drawChildren();
-                // Reset the screen clipping so we can draw the next title.
-                getScreen().resetClipping();
+                // Convert the mouse relative x/y to menu coordinates
+                assert (mouse.getX() == mouse.getAbsoluteX());
+                assert (mouse.getY() == mouse.getAbsoluteY());
+                if (subMenus.size() > 0) {
+                    menu = subMenus.get(subMenus.size() - 1);
+                }
+                mouse.setX(mouse.getX() - menu.getX());
+                mouse.setY(mouse.getY() - menu.getY());
             }
-            x += menu.getTitle().length() + 2;
-        }
-
-        for (TMenu menu: subMenus) {
-            // Reset the screen clipping so we can draw the next sub-menu.
-            getScreen().resetClipping();
-            menu.drawChildren();
+            menu.handleEvent(event);
+            return;
         }
 
-        // Draw the status bar of the top-level window
-        TStatusBar statusBar = null;
-        if (topLevel != null) {
-            statusBar = topLevel.getStatusBar();
-        }
-        if (statusBar != null) {
-            getScreen().resetClipping();
-            statusBar.setWidth(getScreen().getWidth());
-            statusBar.setY(getScreen().getHeight() - topLevel.getY());
-            statusBar.draw();
-        } else {
-            CellAttributes barColor = new CellAttributes();
-            barColor.setTo(getTheme().getColor("tstatusbar.text"));
-            getScreen().hLineXY(0, desktopBottom, getScreen().getWidth(), ' ',
-                barColor);
-        }
+        if (event instanceof TKeypressEvent) {
+            TKeypressEvent keypress = (TKeypressEvent) event;
 
-        // Draw the mouse pointer
-        invertCell(mouseX, mouseY);
-        oldMouseX = mouseX;
-        oldMouseY = mouseY;
+            // See if this key matches an accelerator, and is not being
+            // shortcutted by the active window, and if so dispatch the menu
+            // event.
+            boolean windowWillShortcut = false;
+            if (activeWindow != null) {
+                assert (activeWindow.isShown());
+                if (activeWindow.isShortcutKeypress(keypress.getKey())) {
+                    // We do not process this key, it will be passed to the
+                    // window instead.
+                    windowWillShortcut = true;
+                }
+            }
 
-        // Place the cursor if it is visible
-        if (!menuIsActive) {
-            TWidget activeWidget = null;
-            if (sorted.size() > 0) {
-                activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
-                if (activeWidget.isCursorVisible()) {
-                    if ((activeWidget.getCursorAbsoluteY() < desktopBottom)
-                        && (activeWidget.getCursorAbsoluteY() > desktopTop)
-                    ) {
-                        getScreen().putCursor(true,
-                            activeWidget.getCursorAbsoluteX(),
-                            activeWidget.getCursorAbsoluteY());
-                        cursor = true;
-                    } else {
-                        getScreen().putCursor(false,
-                            activeWidget.getCursorAbsoluteX(),
-                            activeWidget.getCursorAbsoluteY());
-                        cursor = false;
+            if (!windowWillShortcut && !modalWindowActive()) {
+                TKeypress keypressLowercase = keypress.getKey().toLowerCase();
+                TMenuItem item = null;
+                synchronized (accelerators) {
+                    item = accelerators.get(keypressLowercase);
+                }
+                if (item != null) {
+                    if (item.isEnabled()) {
+                        // Let the menu item dispatch
+                        item.dispatch();
+                        return;
                     }
                 }
+
+                // Handle the keypress
+                if (onKeypress(keypress)) {
+                    return;
+                }
             }
         }
 
-        // Kill the cursor
-        if (!cursor) {
-            getScreen().hideCursor();
+        if (event instanceof TCommandEvent) {
+            if (onCommand((TCommandEvent) event)) {
+                return;
+            }
         }
 
-        // Flush the screen contents
-        if (getScreen().isDirty()) {
-            backend.flushScreen();
+        if (event instanceof TMenuEvent) {
+            if (onMenu((TMenuEvent) event)) {
+                return;
+            }
         }
 
-        repaint = false;
-    }
+        // Dispatch events to the active window -------------------------------
+        boolean dispatchToDesktop = true;
+        TWindow window = activeWindow;
+        if (window != null) {
+            assert (window.isActive());
+            assert (window.isShown());
+            if (event instanceof TMouseEvent) {
+                TMouseEvent mouse = (TMouseEvent) event;
+                // Convert the mouse relative x/y to window coordinates
+                assert (mouse.getX() == mouse.getAbsoluteX());
+                assert (mouse.getY() == mouse.getAbsoluteY());
+                mouse.setX(mouse.getX() - window.getX());
+                mouse.setY(mouse.getY() - window.getY());
 
-    // ------------------------------------------------------------------------
-    // Main loop --------------------------------------------------------------
-    // ------------------------------------------------------------------------
+                if (doubleClick != null) {
+                    doubleClick.setX(doubleClick.getX() - window.getX());
+                    doubleClick.setY(doubleClick.getY() - window.getY());
+                }
 
-    /**
-     * Force this application to exit.
-     */
-    public void exit() {
-        quit = true;
-        synchronized (this) {
-            this.notify();
+                if (window.mouseWouldHit(mouse)) {
+                    dispatchToDesktop = false;
+                }
+            } else if (event instanceof TKeypressEvent) {
+                dispatchToDesktop = false;
+            }
+
+            if (debugEvents) {
+                System.err.printf("TApplication dispatch event: %s\n",
+                    event);
+            }
+            window.handleEvent(event);
+            if (doubleClick != null) {
+                window.handleEvent(doubleClick);
+            }
+        }
+        if (dispatchToDesktop) {
+            // This event is fair game for the desktop to process.
+            if (desktop != null) {
+                desktop.handleEvent(event);
+                if (doubleClick != null) {
+                    desktop.handleEvent(doubleClick);
+                }
+            }
         }
     }
 
     /**
-     * Run this application until it exits.
+     * Dispatch one event to the appropriate widget or application-level
+     * event handler.  This is the secondary event handler used by certain
+     * special dialogs (currently TMessageBox and TFileOpenBox).
+     *
+     * @param event the input event to consume
+     * @see #primaryHandleEvent(TInputEvent event)
      */
-    public void run() {
-        // Start the main consumer thread
-        primaryEventHandler = new WidgetEventHandler(this, true);
-        (new Thread(primaryEventHandler)).start();
+    private void secondaryHandleEvent(final TInputEvent event) {
+        TMouseEvent doubleClick = null;
 
-        started = true;
+        // Peek at the mouse position
+        if (event instanceof TMouseEvent) {
+            TMouseEvent mouse = (TMouseEvent) event;
+            if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
+                oldMouseX = mouseX;
+                oldMouseY = mouseY;
+                mouseX = mouse.getX();
+                mouseY = mouse.getY();
+            } else {
+                if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
+                    if ((mouse.getTime().getTime() - lastMouseUpTime) <
+                        doubleClickTime) {
 
-        while (!quit) {
-            synchronized (this) {
-                boolean doWait = false;
+                        // This is a double-click.
+                        doubleClick = new TMouseEvent(TMouseEvent.Type.
+                            MOUSE_DOUBLE_CLICK,
+                            mouse.getX(), mouse.getY(),
+                            mouse.getAbsoluteX(), mouse.getAbsoluteY(),
+                            mouse.isMouse1(), mouse.isMouse2(),
+                            mouse.isMouse3(),
+                            mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
 
-                synchronized (fillEventQueue) {
-                    if (fillEventQueue.size() == 0) {
-                        doWait = true;
+                    } else {
+                        // The first click of a potential double-click.
+                        lastMouseUpTime = mouse.getTime().getTime();
                     }
                 }
+            }
+        }
 
-                if (doWait) {
-                    // No I/O to dispatch, so wait until the backend
-                    // provides new I/O.
-                    try {
-                        if (debugThreads) {
-                            System.err.println(System.currentTimeMillis() +
-                                " MAIN sleep");
-                        }
+        secondaryEventReceiver.handleEvent(event);
+        if (doubleClick != null) {
+            secondaryEventReceiver.handleEvent(doubleClick);
+        }
+    }
 
-                        this.wait();
+    /**
+     * Enable a widget to override the primary event thread.
+     *
+     * @param widget widget that will receive events
+     */
+    public final void enableSecondaryEventReceiver(final TWidget widget) {
+        if (debugThreads) {
+            System.err.println(System.currentTimeMillis() +
+                " enableSecondaryEventReceiver()");
+        }
 
-                        if (debugThreads) {
-                            System.err.println(System.currentTimeMillis() +
-                                " MAIN AWAKE");
-                        }
-                    } catch (InterruptedException e) {
-                        // I'm awake and don't care why, let's see what's
-                        // going on out there.
-                    }
+        assert (secondaryEventReceiver == null);
+        assert (secondaryEventHandler == null);
+        assert ((widget instanceof TMessageBox)
+            || (widget instanceof TFileOpenBox));
+        secondaryEventReceiver = widget;
+        secondaryEventHandler = new WidgetEventHandler(this, false);
+
+        (new Thread(secondaryEventHandler)).start();
+    }
+
+    /**
+     * Yield to the secondary thread.
+     */
+    public final void yield() {
+        assert (secondaryEventReceiver != null);
+
+        while (secondaryEventReceiver != null) {
+            synchronized (primaryEventHandler) {
+                try {
+                    primaryEventHandler.wait();
+                } catch (InterruptedException e) {
+                    // SQUASH
                 }
+            }
+        }
+    }
 
-            } // synchronized (this)
+    /**
+     * Do stuff when there is no user input.
+     */
+    private void doIdle() {
+        if (debugThreads) {
+            System.err.printf(System.currentTimeMillis() + " " +
+                Thread.currentThread() + " doIdle()\n");
+        }
 
-            synchronized (fillEventQueue) {
-                // Pull any pending I/O events
-                backend.getEvents(fillEventQueue);
+        synchronized (timers) {
 
-                // Dispatch each event to the appropriate handler, one at a
-                // time.
-                for (;;) {
-                    TInputEvent event = null;
-                    if (fillEventQueue.size() == 0) {
-                        break;
+            if (debugThreads) {
+                System.err.printf(System.currentTimeMillis() + " " +
+                    Thread.currentThread() + " doIdle() 2\n");
+            }
+
+            // Run any timers that have timed out
+            Date now = new Date();
+            List<TTimer> keepTimers = new LinkedList<TTimer>();
+            for (TTimer timer: timers) {
+                if (timer.getNextTick().getTime() <= now.getTime()) {
+                    // Something might change, so repaint the screen.
+                    repaint = true;
+                    timer.tick();
+                    if (timer.recurring) {
+                        keepTimers.add(timer);
                     }
-                    event = fillEventQueue.remove(0);
-                    metaHandleEvent(event);
+                } else {
+                    keepTimers.add(timer);
                 }
             }
+            timers = keepTimers;
+        }
 
-            // Wake a consumer thread if we have any pending events.
-            if (drainEventQueue.size() > 0) {
-                wakeEventHandler();
-            }
+        // Call onIdle's
+        for (TWindow window: windows) {
+            window.onIdle();
+        }
+        if (desktop != null) {
+            desktop.onIdle();
+        }
+    }
 
-        } // while (!quit)
+    /**
+     * Wake the sleeping active event handler.
+     */
+    private void wakeEventHandler() {
+        if (!started) {
+            return;
+        }
 
-        // Shutdown the event consumer threads
         if (secondaryEventHandler != null) {
             synchronized (secondaryEventHandler) {
                 secondaryEventHandler.notify();
             }
-        }
-        if (primaryEventHandler != null) {
+        } else {
+            assert (primaryEventHandler != null);
             synchronized (primaryEventHandler) {
                 primaryEventHandler.notify();
             }
         }
+    }
 
-        // Shutdown the user I/O thread(s)
-        backend.shutdown();
-
-        // Close all the windows.  This gives them an opportunity to release
-        // resources.
-        closeAllWindows();
+    // ------------------------------------------------------------------------
+    // TApplication -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
+    /**
+     * Get the Backend.
+     *
+     * @return the Backend
+     */
+    public final Backend getBackend() {
+        return backend;
     }
 
     /**
-     * Peek at certain application-level events, add to eventQueue, and wake
-     * up the consuming Thread.
+     * Get the Screen.
      *
-     * @param event the input event to consume
+     * @return the Screen
      */
-    private void metaHandleEvent(final TInputEvent event) {
-
-        if (debugEvents) {
-            System.err.printf(String.format("metaHandleEvents event: %s\n",
-                    event)); System.err.flush();
-        }
-
-        if (quit) {
-            // Do no more processing if the application is already trying
-            // to exit.
-            return;
+    public final Screen getScreen() {
+        if (backend instanceof TWindowBackend) {
+            // We are being rendered to a TWindow.  We can't use its
+            // getScreen() method because that is how it is rendering to a
+            // hardware backend somewhere.  Instead use its getOtherScreen()
+            // method.
+            return ((TWindowBackend) backend).getOtherScreen();
+        } else {
+            return backend.getScreen();
         }
+    }
 
-        // Special application-wide events -------------------------------
-
-        // Abort everything
-        if (event instanceof TCommandEvent) {
-            TCommandEvent command = (TCommandEvent) event;
-            if (command.getCmd().equals(cmAbort)) {
-                exit();
-                return;
-            }
-        }
+    /**
+     * Get the color theme.
+     *
+     * @return the theme
+     */
+    public final ColorTheme getTheme() {
+        return theme;
+    }
 
-        synchronized (drainEventQueue) {
-            // Screen resize
-            if (event instanceof TResizeEvent) {
-                TResizeEvent resize = (TResizeEvent) event;
-                synchronized (getScreen()) {
-                    getScreen().setDimensions(resize.getWidth(),
-                        resize.getHeight());
-                    desktopBottom = getScreen().getHeight() - 1;
-                    mouseX = 0;
-                    mouseY = 0;
-                    oldMouseX = 0;
-                    oldMouseY = 0;
-                }
-                if (desktop != null) {
-                    desktop.setDimensions(0, 0, resize.getWidth(),
-                        resize.getHeight() - 1);
-                }
+    /**
+     * Repaint the screen on the next update.
+     */
+    public void doRepaint() {
+        repaint = true;
+        wakeEventHandler();
+    }
 
-                // Change menu edges if needed.
-                recomputeMenuX();
+    /**
+     * Get Y coordinate of the top edge of the desktop.
+     *
+     * @return Y coordinate of the top edge of the desktop
+     */
+    public final int getDesktopTop() {
+        return desktopTop;
+    }
 
-                // We are dirty, redraw the screen.
-                doRepaint();
-                return;
-            }
+    /**
+     * Get Y coordinate of the bottom edge of the desktop.
+     *
+     * @return Y coordinate of the bottom edge of the desktop
+     */
+    public final int getDesktopBottom() {
+        return desktopBottom;
+    }
 
-            // Put into the main queue
-            drainEventQueue.add(event);
+    /**
+     * Set the TDesktop instance.
+     *
+     * @param desktop a TDesktop instance, or null to remove the one that is
+     * set
+     */
+    public final void setDesktop(final TDesktop desktop) {
+        if (this.desktop != null) {
+            this.desktop.onClose();
         }
+        this.desktop = desktop;
     }
 
     /**
-     * Dispatch one event to the appropriate widget or application-level
-     * event handler.  This is the primary event handler, it has the normal
-     * application-wide event handling.
+     * Get the TDesktop instance.
      *
-     * @param event the input event to consume
-     * @see #secondaryHandleEvent(TInputEvent event)
+     * @return the desktop, or null if it is not set
      */
-    private void primaryHandleEvent(final TInputEvent event) {
-
-        if (debugEvents) {
-            System.err.printf("Handle event: %s\n", event);
-        }
-        TMouseEvent doubleClick = null;
+    public final TDesktop getDesktop() {
+        return desktop;
+    }
 
-        // Special application-wide events -----------------------------------
+    /**
+     * Get the current active window.
+     *
+     * @return the active window, or null if it is not set
+     */
+    public final TWindow getActiveWindow() {
+        return activeWindow;
+    }
 
-        // Peek at the mouse position
-        if (event instanceof TMouseEvent) {
-            TMouseEvent mouse = (TMouseEvent) event;
-            if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
-                oldMouseX = mouseX;
-                oldMouseY = mouseY;
-                mouseX = mouse.getX();
-                mouseY = mouse.getY();
-            } else {
-                if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
-                    if ((mouse.getTime().getTime() - lastMouseUpTime) <
-                        doubleClickTime) {
+    /**
+     * Get a (shallow) copy of the window list.
+     *
+     * @return a copy of the list of windows for this application
+     */
+    public final List<TWindow> getAllWindows() {
+        List<TWindow> result = new LinkedList<TWindow>();
+        result.addAll(windows);
+        return result;
+    }
 
-                        // This is a double-click.
-                        doubleClick = new TMouseEvent(TMouseEvent.Type.
-                            MOUSE_DOUBLE_CLICK,
-                            mouse.getX(), mouse.getY(),
-                            mouse.getAbsoluteX(), mouse.getAbsoluteY(),
-                            mouse.isMouse1(), mouse.isMouse2(),
-                            mouse.isMouse3(),
-                            mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+    /**
+     * Get focusFollowsMouse flag.
+     *
+     * @return true if focus follows mouse: windows automatically raised if
+     * the mouse passes over them
+     */
+    public boolean getFocusFollowsMouse() {
+        return focusFollowsMouse;
+    }
 
-                    } else {
-                        // The first click of a potential double-click.
-                        lastMouseUpTime = mouse.getTime().getTime();
-                    }
-                }
-            }
+    /**
+     * Set focusFollowsMouse flag.
+     *
+     * @param focusFollowsMouse if true, focus follows mouse: windows
+     * automatically raised if the mouse passes over them
+     */
+    public void setFocusFollowsMouse(final boolean focusFollowsMouse) {
+        this.focusFollowsMouse = focusFollowsMouse;
+    }
 
-            // See if we need to switch focus to another window or the menu
-            checkSwitchFocus((TMouseEvent) event);
-        }
+    /**
+     * Display the about dialog.
+     */
+    protected void showAboutDialog() {
+        messageBox(i18n.getString("aboutDialogTitle"),
+            MessageFormat.format(i18n.getString("aboutDialogText"),
+                this.getClass().getPackage().getImplementationVersion()),
+            TMessageBox.Type.OK);
+    }
 
-        // Handle menu events
-        if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
-            TMenu menu = activeMenu;
+    // ------------------------------------------------------------------------
+    // Screen refresh loop ----------------------------------------------------
+    // ------------------------------------------------------------------------
 
-            if (event instanceof TMouseEvent) {
-                TMouseEvent mouse = (TMouseEvent) event;
+    /**
+     * Invert the cell color at a position.  This is used to track the mouse.
+     *
+     * @param x column position
+     * @param y row position
+     */
+    private void invertCell(final int x, final int y) {
+        if (debugThreads) {
+            System.err.printf("%d %s invertCell() %d %d\n",
+                System.currentTimeMillis(), Thread.currentThread(), x, y);
+        }
+        CellAttributes attr = getScreen().getAttrXY(x, y);
+        attr.setForeColor(attr.getForeColor().invert());
+        attr.setBackColor(attr.getBackColor().invert());
+        getScreen().putAttrXY(x, y, attr, false);
+    }
 
-                while (subMenus.size() > 0) {
-                    TMenu subMenu = subMenus.get(subMenus.size() - 1);
-                    if (subMenu.mouseWouldHit(mouse)) {
-                        break;
-                    }
-                    if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
-                        && (!mouse.isMouse1())
-                        && (!mouse.isMouse2())
-                        && (!mouse.isMouse3())
-                        && (!mouse.isMouseWheelUp())
-                        && (!mouse.isMouseWheelDown())
-                    ) {
-                        break;
-                    }
-                    // We navigated away from a sub-menu, so close it
-                    closeSubMenu();
-                }
+    /**
+     * Draw everything.
+     */
+    private void drawAll() {
+        boolean menuIsActive = false;
 
-                // Convert the mouse relative x/y to menu coordinates
-                assert (mouse.getX() == mouse.getAbsoluteX());
-                assert (mouse.getY() == mouse.getAbsoluteY());
-                if (subMenus.size() > 0) {
-                    menu = subMenus.get(subMenus.size() - 1);
-                }
-                mouse.setX(mouse.getX() - menu.getX());
-                mouse.setY(mouse.getY() - menu.getY());
-            }
-            menu.handleEvent(event);
-            return;
+        if (debugThreads) {
+            System.err.printf("%d %s drawAll() enter\n",
+                System.currentTimeMillis(), Thread.currentThread());
         }
 
-        if (event instanceof TKeypressEvent) {
-            TKeypressEvent keypress = (TKeypressEvent) event;
-
-            // See if this key matches an accelerator, and is not being
-            // shortcutted by the active window, and if so dispatch the menu
-            // event.
-            boolean windowWillShortcut = false;
-            if (activeWindow != null) {
-                assert (activeWindow.isShown());
-                if (activeWindow.isShortcutKeypress(keypress.getKey())) {
-                    // We do not process this key, it will be passed to the
-                    // window instead.
-                    windowWillShortcut = true;
-                }
+        if (!repaint) {
+            if (debugThreads) {
+                System.err.printf("%d %s drawAll() !repaint\n",
+                    System.currentTimeMillis(), Thread.currentThread());
             }
-
-            if (!windowWillShortcut && !modalWindowActive()) {
-                TKeypress keypressLowercase = keypress.getKey().toLowerCase();
-                TMenuItem item = null;
-                synchronized (accelerators) {
-                    item = accelerators.get(keypressLowercase);
-                }
-                if (item != null) {
-                    if (item.isEnabled()) {
-                        // Let the menu item dispatch
-                        item.dispatch();
-                        return;
-                    }
+            synchronized (getScreen()) {
+                if ((oldMouseX != mouseX) || (oldMouseY != mouseY)) {
+                    // The only thing that has happened is the mouse moved.
+                    // Clear the old position and draw the new position.
+                    invertCell(oldMouseX, oldMouseY);
+                    invertCell(mouseX, mouseY);
+                    oldMouseX = mouseX;
+                    oldMouseY = mouseY;
                 }
-
-                // Handle the keypress
-                if (onKeypress(keypress)) {
-                    return;
+                if (getScreen().isDirty()) {
+                    backend.flushScreen();
                 }
-            }
-        }
-
-        if (event instanceof TCommandEvent) {
-            if (onCommand((TCommandEvent) event)) {
                 return;
             }
         }
 
-        if (event instanceof TMenuEvent) {
-            if (onMenu((TMenuEvent) event)) {
-                return;
-            }
+        if (debugThreads) {
+            System.err.printf("%d %s drawAll() REDRAW\n",
+                System.currentTimeMillis(), Thread.currentThread());
         }
 
-        // Dispatch events to the active window -------------------------------
-        boolean dispatchToDesktop = true;
-        TWindow window = activeWindow;
-        if (window != null) {
-            assert (window.isActive());
-            assert (window.isShown());
-            if (event instanceof TMouseEvent) {
-                TMouseEvent mouse = (TMouseEvent) event;
-                // Convert the mouse relative x/y to window coordinates
-                assert (mouse.getX() == mouse.getAbsoluteX());
-                assert (mouse.getY() == mouse.getAbsoluteY());
-                mouse.setX(mouse.getX() - window.getX());
-                mouse.setY(mouse.getY() - window.getY());
+        // If true, the cursor is not visible
+        boolean cursor = false;
 
-                if (doubleClick != null) {
-                    doubleClick.setX(doubleClick.getX() - window.getX());
-                    doubleClick.setY(doubleClick.getY() - window.getY());
-                }
+        // Start with a clean screen
+        getScreen().clear();
 
-                if (window.mouseWouldHit(mouse)) {
-                    dispatchToDesktop = false;
-                }
-            } else if (event instanceof TKeypressEvent) {
-                dispatchToDesktop = false;
-            }
+        // Draw the desktop
+        if (desktop != null) {
+            desktop.drawChildren();
+        }
 
-            if (debugEvents) {
-                System.err.printf("TApplication dispatch event: %s\n",
-                    event);
-            }
-            window.handleEvent(event);
-            if (doubleClick != null) {
-                window.handleEvent(doubleClick);
+        // Draw each window in reverse Z order
+        List<TWindow> sorted = new LinkedList<TWindow>(windows);
+        Collections.sort(sorted);
+        TWindow topLevel = null;
+        if (sorted.size() > 0) {
+            topLevel = sorted.get(0);
+        }
+        Collections.reverse(sorted);
+        for (TWindow window: sorted) {
+            if (window.isShown()) {
+                window.drawChildren();
             }
         }
-        if (dispatchToDesktop) {
-            // This event is fair game for the desktop to process.
-            if (desktop != null) {
-                desktop.handleEvent(event);
-                if (doubleClick != null) {
-                    desktop.handleEvent(doubleClick);
-                }
+
+        // Draw the blank menubar line - reset the screen clipping first so
+        // it won't trim it out.
+        getScreen().resetClipping();
+        getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
+            theme.getColor("tmenu"));
+        // Now draw the menus.
+        int x = 1;
+        for (TMenu menu: menus) {
+            CellAttributes menuColor;
+            CellAttributes menuMnemonicColor;
+            if (menu.isActive()) {
+                menuIsActive = true;
+                menuColor = theme.getColor("tmenu.highlighted");
+                menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
+                topLevel = menu;
+            } else {
+                menuColor = theme.getColor("tmenu");
+                menuMnemonicColor = theme.getColor("tmenu.mnemonic");
+            }
+            // Draw the menu title
+            getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ',
+                menuColor);
+            getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor);
+            // Draw the highlight character
+            getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(),
+                0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
+
+            if (menu.isActive()) {
+                menu.drawChildren();
+                // Reset the screen clipping so we can draw the next title.
+                getScreen().resetClipping();
             }
+            x += menu.getTitle().length() + 2;
         }
-    }
 
-    /**
-     * Dispatch one event to the appropriate widget or application-level
-     * event handler.  This is the secondary event handler used by certain
-     * special dialogs (currently TMessageBox and TFileOpenBox).
-     *
-     * @param event the input event to consume
-     * @see #primaryHandleEvent(TInputEvent event)
-     */
-    private void secondaryHandleEvent(final TInputEvent event) {
-        TMouseEvent doubleClick = null;
+        for (TMenu menu: subMenus) {
+            // Reset the screen clipping so we can draw the next sub-menu.
+            getScreen().resetClipping();
+            menu.drawChildren();
+        }
 
-        // Peek at the mouse position
-        if (event instanceof TMouseEvent) {
-            TMouseEvent mouse = (TMouseEvent) event;
-            if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
-                oldMouseX = mouseX;
-                oldMouseY = mouseY;
-                mouseX = mouse.getX();
-                mouseY = mouse.getY();
-            } else {
-                if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
-                    if ((mouse.getTime().getTime() - lastMouseUpTime) <
-                        doubleClickTime) {
+        // Draw the status bar of the top-level window
+        TStatusBar statusBar = null;
+        if (topLevel != null) {
+            statusBar = topLevel.getStatusBar();
+        }
+        if (statusBar != null) {
+            getScreen().resetClipping();
+            statusBar.setWidth(getScreen().getWidth());
+            statusBar.setY(getScreen().getHeight() - topLevel.getY());
+            statusBar.draw();
+        } else {
+            CellAttributes barColor = new CellAttributes();
+            barColor.setTo(getTheme().getColor("tstatusbar.text"));
+            getScreen().hLineXY(0, desktopBottom, getScreen().getWidth(), ' ',
+                barColor);
+        }
 
-                        // This is a double-click.
-                        doubleClick = new TMouseEvent(TMouseEvent.Type.
-                            MOUSE_DOUBLE_CLICK,
-                            mouse.getX(), mouse.getY(),
-                            mouse.getAbsoluteX(), mouse.getAbsoluteY(),
-                            mouse.isMouse1(), mouse.isMouse2(),
-                            mouse.isMouse3(),
-                            mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+        // Draw the mouse pointer
+        invertCell(mouseX, mouseY);
+        oldMouseX = mouseX;
+        oldMouseY = mouseY;
 
+        // Place the cursor if it is visible
+        if (!menuIsActive) {
+            TWidget activeWidget = null;
+            if (sorted.size() > 0) {
+                activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
+                if (activeWidget.isCursorVisible()) {
+                    if ((activeWidget.getCursorAbsoluteY() < desktopBottom)
+                        && (activeWidget.getCursorAbsoluteY() > desktopTop)
+                    ) {
+                        getScreen().putCursor(true,
+                            activeWidget.getCursorAbsoluteX(),
+                            activeWidget.getCursorAbsoluteY());
+                        cursor = true;
                     } else {
-                        // The first click of a potential double-click.
-                        lastMouseUpTime = mouse.getTime().getTime();
+                        getScreen().putCursor(false,
+                            activeWidget.getCursorAbsoluteX(),
+                            activeWidget.getCursorAbsoluteY());
+                        cursor = false;
                     }
                 }
             }
         }
 
-        secondaryEventReceiver.handleEvent(event);
-        if (doubleClick != null) {
-            secondaryEventReceiver.handleEvent(doubleClick);
+        // Kill the cursor
+        if (!cursor) {
+            getScreen().hideCursor();
         }
-    }
 
-    /**
-     * Enable a widget to override the primary event thread.
-     *
-     * @param widget widget that will receive events
-     */
-    public final void enableSecondaryEventReceiver(final TWidget widget) {
-        if (debugThreads) {
-            System.err.println(System.currentTimeMillis() +
-                " enableSecondaryEventReceiver()");
+        // Flush the screen contents
+        if (getScreen().isDirty()) {
+            backend.flushScreen();
         }
 
-        assert (secondaryEventReceiver == null);
-        assert (secondaryEventHandler == null);
-        assert ((widget instanceof TMessageBox)
-            || (widget instanceof TFileOpenBox));
-        secondaryEventReceiver = widget;
-        secondaryEventHandler = new WidgetEventHandler(this, false);
-
-        (new Thread(secondaryEventHandler)).start();
-    }
-
-    /**
-     * Yield to the secondary thread.
-     */
-    public final void yield() {
-        assert (secondaryEventReceiver != null);
-
-        while (secondaryEventReceiver != null) {
-            synchronized (primaryEventHandler) {
-                try {
-                    primaryEventHandler.wait();
-                } catch (InterruptedException e) {
-                    // SQUASH
-                }
-            }
-        }
+        repaint = false;
     }
 
     /**
-     * Do stuff when there is no user input.
+     * Force this application to exit.
      */
-    private void doIdle() {
-        if (debugThreads) {
-            System.err.printf(System.currentTimeMillis() + " " +
-                Thread.currentThread() + " doIdle()\n");
-        }
-
-        synchronized (timers) {
-
-            if (debugThreads) {
-                System.err.printf(System.currentTimeMillis() + " " +
-                    Thread.currentThread() + " doIdle() 2\n");
-            }
-
-            // Run any timers that have timed out
-            Date now = new Date();
-            List<TTimer> keepTimers = new LinkedList<TTimer>();
-            for (TTimer timer: timers) {
-                if (timer.getNextTick().getTime() <= now.getTime()) {
-                    // Something might change, so repaint the screen.
-                    repaint = true;
-                    timer.tick();
-                    if (timer.recurring) {
-                        keepTimers.add(timer);
-                    }
-                } else {
-                    keepTimers.add(timer);
-                }
-            }
-            timers = keepTimers;
-        }
-
-        // Call onIdle's
-        for (TWindow window: windows) {
-            window.onIdle();
-        }
-        if (desktop != null) {
-            desktop.onIdle();
+    public void exit() {
+        quit = true;
+        synchronized (this) {
+            this.notify();
         }
     }
 
@@ -1770,7 +1906,7 @@ public class TApplication implements Runnable {
      *
      * @param window new window to add
      */
-    public final void addWindow(final TWindow window) {
+    public final void addWindowToApplication(final TWindow window) {
 
         // Do not add menu windows to the window list.
         if (window instanceof TMenu) {
@@ -1816,7 +1952,9 @@ public class TApplication implements Runnable {
             }
 
             if (((window.flags & TWindow.CENTERED) == 0)
-                && smartWindowPlacement) {
+                && ((window.flags & TWindow.ABSOLUTEXY) == 0)
+                && (smartWindowPlacement == true)
+            ) {
 
                 doSmartPlacement(window);
             }
@@ -2577,139 +2715,6 @@ public class TApplication implements Runnable {
         return helpMenu;
     }
 
-    // ------------------------------------------------------------------------
-    // Event handlers ---------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Method that TApplication subclasses can override to handle menu or
-     * posted command events.
-     *
-     * @param command command event
-     * @return if true, this event was consumed
-     */
-    protected boolean onCommand(final TCommandEvent command) {
-        // Default: handle cmExit
-        if (command.equals(cmExit)) {
-            if (messageBox(i18n.getString("exitDialogTitle"),
-                    i18n.getString("exitDialogText"),
-                    TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
-                exit();
-            }
-            return true;
-        }
-
-        if (command.equals(cmShell)) {
-            openTerminal(0, 0, TWindow.RESIZABLE);
-            return true;
-        }
-
-        if (command.equals(cmTile)) {
-            tileWindows();
-            return true;
-        }
-        if (command.equals(cmCascade)) {
-            cascadeWindows();
-            return true;
-        }
-        if (command.equals(cmCloseAll)) {
-            closeAllWindows();
-            return true;
-        }
-
-        if (command.equals(cmMenu)) {
-            if (!modalWindowActive() && (activeMenu == null)) {
-                if (menus.size() > 0) {
-                    menus.get(0).setActive(true);
-                    activeMenu = menus.get(0);
-                    return true;
-                }
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Method that TApplication subclasses can override to handle menu
-     * events.
-     *
-     * @param menu menu event
-     * @return if true, this event was consumed
-     */
-    protected boolean onMenu(final TMenuEvent menu) {
-
-        // Default: handle MID_EXIT
-        if (menu.getId() == TMenu.MID_EXIT) {
-            if (messageBox(i18n.getString("exitDialogTitle"),
-                    i18n.getString("exitDialogText"),
-                    TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
-                exit();
-            }
-            return true;
-        }
-
-        if (menu.getId() == TMenu.MID_SHELL) {
-            openTerminal(0, 0, TWindow.RESIZABLE);
-            return true;
-        }
-
-        if (menu.getId() == TMenu.MID_TILE) {
-            tileWindows();
-            return true;
-        }
-        if (menu.getId() == TMenu.MID_CASCADE) {
-            cascadeWindows();
-            return true;
-        }
-        if (menu.getId() == TMenu.MID_CLOSE_ALL) {
-            closeAllWindows();
-            return true;
-        }
-        if (menu.getId() == TMenu.MID_ABOUT) {
-            showAboutDialog();
-            return true;
-        }
-        if (menu.getId() == TMenu.MID_REPAINT) {
-            doRepaint();
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Method that TApplication subclasses can override to handle keystrokes.
-     *
-     * @param keypress keystroke event
-     * @return if true, this event was consumed
-     */
-    protected boolean onKeypress(final TKeypressEvent keypress) {
-        // Default: only menu shortcuts
-
-        // Process Alt-F, Alt-E, etc. menu shortcut keys
-        if (!keypress.getKey().isFnKey()
-            && keypress.getKey().isAlt()
-            && !keypress.getKey().isCtrl()
-            && (activeMenu == null)
-            && !modalWindowActive()
-        ) {
-
-            assert (subMenus.size() == 0);
-
-            for (TMenu menu: menus) {
-                if (Character.toLowerCase(menu.getMnemonic().getShortcut())
-                    == Character.toLowerCase(keypress.getKey().getChar())
-                ) {
-                    activeMenu = menu;
-                    menu.setActive(true);
-                    return true;
-                }
-            }
-        }
-
-        return false;
-    }
-
     // ------------------------------------------------------------------------
     // TTimer management ------------------------------------------------------
     // ------------------------------------------------------------------------
index 255dd990738d369490a288d993dd24dd24890c49..d4e7c8952a208682febdf59457476807cc8910d5 100644 (file)
@@ -44,20 +44,15 @@ import static jexer.TKeypress.*;
  */
 public final class TButton extends TWidget {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The shortcut and button text.
      */
     private MnemonicString mnemonic;
 
-    /**
-     * Get the mnemonic string for this button.
-     *
-     * @return mnemonic string
-     */
-    public MnemonicString getMnemonic() {
-        return mnemonic;
-    }
-
     /**
      * Remember mouse state.
      */
@@ -73,16 +68,9 @@ public final class TButton extends TWidget {
      */
     private TAction action;
 
-    /**
-     * Act as though the button was pressed.  This is useful for other UI
-     * elements to get the same action as if the user clicked the button.
-     */
-    public void dispatch() {
-        if (action != null) {
-            action.DO();
-            inButtonPress = false;
-        }
-    }
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Private constructor.
@@ -122,6 +110,10 @@ public final class TButton extends TWidget {
         this.action = action;
     }
 
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Returns true if the mouse is currently on the button.
      *
@@ -142,55 +134,6 @@ public final class TButton extends TWidget {
         return false;
     }
 
-    /**
-     * Draw a button with a shadow.
-     */
-    @Override
-    public void draw() {
-        CellAttributes buttonColor;
-        CellAttributes menuMnemonicColor;
-        CellAttributes shadowColor = new CellAttributes();
-        shadowColor.setTo(getWindow().getBackground());
-        shadowColor.setForeColor(Color.BLACK);
-        shadowColor.setBold(false);
-
-        if (!isEnabled()) {
-            buttonColor = getTheme().getColor("tbutton.disabled");
-            menuMnemonicColor = getTheme().getColor("tbutton.disabled");
-        } else if (isAbsoluteActive()) {
-            buttonColor = getTheme().getColor("tbutton.active");
-            menuMnemonicColor = getTheme().getColor("tbutton.mnemonic.highlighted");
-        } else {
-            buttonColor = getTheme().getColor("tbutton.inactive");
-            menuMnemonicColor = getTheme().getColor("tbutton.mnemonic");
-        }
-
-        if (inButtonPress) {
-            getScreen().putCharXY(1, 0, ' ', buttonColor);
-            getScreen().putStringXY(2, 0, mnemonic.getRawLabel(), buttonColor);
-            getScreen().putCharXY(getWidth() - 1, 0, ' ', buttonColor);
-        } else {
-            getScreen().putCharXY(0, 0, ' ', buttonColor);
-            getScreen().putStringXY(1, 0, mnemonic.getRawLabel(), buttonColor);
-            getScreen().putCharXY(getWidth() - 2, 0, ' ', buttonColor);
-
-            getScreen().putCharXY(getWidth() - 1, 0,
-                GraphicsChars.CP437[0xDC], shadowColor);
-            getScreen().hLineXY(1, 1, getWidth() - 1,
-                GraphicsChars.CP437[0xDF], shadowColor);
-        }
-        if (mnemonic.getShortcutIdx() >= 0) {
-            if (inButtonPress) {
-                getScreen().putCharXY(2 + mnemonic.getShortcutIdx(), 0,
-                    mnemonic.getShortcut(), menuMnemonicColor);
-            } else {
-                getScreen().putCharXY(1 + mnemonic.getShortcutIdx(), 0,
-                    mnemonic.getShortcut(), menuMnemonicColor);
-            }
-
-        }
-    }
-
     /**
      * Handle mouse button presses.
      *
@@ -255,4 +198,81 @@ public final class TButton extends TWidget {
         super.onKeypress(keypress);
     }
 
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw a button with a shadow.
+     */
+    @Override
+    public void draw() {
+        CellAttributes buttonColor;
+        CellAttributes menuMnemonicColor;
+        CellAttributes shadowColor = new CellAttributes();
+        shadowColor.setTo(getWindow().getBackground());
+        shadowColor.setForeColor(Color.BLACK);
+        shadowColor.setBold(false);
+
+        if (!isEnabled()) {
+            buttonColor = getTheme().getColor("tbutton.disabled");
+            menuMnemonicColor = getTheme().getColor("tbutton.disabled");
+        } else if (isAbsoluteActive()) {
+            buttonColor = getTheme().getColor("tbutton.active");
+            menuMnemonicColor = getTheme().getColor("tbutton.mnemonic.highlighted");
+        } else {
+            buttonColor = getTheme().getColor("tbutton.inactive");
+            menuMnemonicColor = getTheme().getColor("tbutton.mnemonic");
+        }
+
+        if (inButtonPress) {
+            getScreen().putCharXY(1, 0, ' ', buttonColor);
+            getScreen().putStringXY(2, 0, mnemonic.getRawLabel(), buttonColor);
+            getScreen().putCharXY(getWidth() - 1, 0, ' ', buttonColor);
+        } else {
+            getScreen().putCharXY(0, 0, ' ', buttonColor);
+            getScreen().putStringXY(1, 0, mnemonic.getRawLabel(), buttonColor);
+            getScreen().putCharXY(getWidth() - 2, 0, ' ', buttonColor);
+
+            getScreen().putCharXY(getWidth() - 1, 0,
+                GraphicsChars.CP437[0xDC], shadowColor);
+            getScreen().hLineXY(1, 1, getWidth() - 1,
+                GraphicsChars.CP437[0xDF], shadowColor);
+        }
+        if (mnemonic.getShortcutIdx() >= 0) {
+            if (inButtonPress) {
+                getScreen().putCharXY(2 + mnemonic.getShortcutIdx(), 0,
+                    mnemonic.getShortcut(), menuMnemonicColor);
+            } else {
+                getScreen().putCharXY(1 + mnemonic.getShortcutIdx(), 0,
+                    mnemonic.getShortcut(), menuMnemonicColor);
+            }
+
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // TButton ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the mnemonic string for this button.
+     *
+     * @return mnemonic string
+     */
+    public MnemonicString getMnemonic() {
+        return mnemonic;
+    }
+
+    /**
+     * Act as though the button was pressed.  This is useful for other UI
+     * elements to get the same action as if the user clicked the button.
+     */
+    public void dispatch() {
+        if (action != null) {
+            action.DO();
+            inButtonPress = false;
+        }
+    }
+
 }
index 78c83c83cfe59ba17e245cf1db2878d97b8c820a..86933a73afd147519393c98757abe027929f6760 100644 (file)
@@ -39,34 +39,24 @@ import jexer.event.TMouseEvent;
  */
 public final class TCheckbox extends TWidget {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Checkbox state, true means checked.
      */
     private boolean checked = false;
 
-    /**
-     * Get checked value.
-     *
-     * @return if true, this is checked
-     */
-    public boolean isChecked() {
-        return checked;
-    }
-
-    /**
-     * Set checked value.
-     *
-     * @param checked new checked value.
-     */
-    public void setChecked(final boolean checked) {
-        this.checked = checked;
-    }
-
     /**
      * Label for this checkbox.
      */
     private String label;
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Public constructor.
      *
@@ -77,7 +67,7 @@ public final class TCheckbox extends TWidget {
      * @param checked initial check state
      */
     public TCheckbox(final TWidget parent, final int x, final int y,
-            final String label, final boolean checked) {
+        final String label, final boolean checked) {
 
         // Set parent and window
         super(parent, x, y, label.length() + 4, 1);
@@ -89,6 +79,10 @@ public final class TCheckbox extends TWidget {
         setCursorX(1);
     }
 
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Returns true if the mouse is currently on the checkbox.
      *
@@ -97,14 +91,47 @@ public final class TCheckbox extends TWidget {
      */
     private boolean mouseOnCheckbox(final TMouseEvent mouse) {
         if ((mouse.getY() == 0)
-                && (mouse.getX() >= 0)
-                && (mouse.getX() <= 2)
-                ) {
+            && (mouse.getX() >= 0)
+            && (mouse.getX() <= 2)
+        ) {
             return true;
         }
         return false;
     }
 
+    /**
+     * Handle mouse checkbox presses.
+     *
+     * @param mouse mouse button down event
+     */
+    @Override
+    public void onMouseDown(final TMouseEvent mouse) {
+        if ((mouseOnCheckbox(mouse)) && (mouse.isMouse1())) {
+            // Switch state
+            checked = !checked;
+        }
+    }
+
+    /**
+     * Handle keystrokes.
+     *
+     * @param keypress keystroke event
+     */
+    @Override
+    public void onKeypress(final TKeypressEvent keypress) {
+        if (keypress.equals(kbSpace)) {
+            checked = !checked;
+            return;
+        }
+
+        // Pass to parent for the things we don't care about.
+        super.onKeypress(keypress);
+    }
+
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Draw a checkbox with label.
      */
@@ -128,33 +155,26 @@ public final class TCheckbox extends TWidget {
         getScreen().putStringXY(4, 0, label, checkboxColor);
     }
 
+    // ------------------------------------------------------------------------
+    // TCheckbox --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
-     * Handle mouse checkbox presses.
+     * Get checked value.
      *
-     * @param mouse mouse button down event
+     * @return if true, this is checked
      */
-    @Override
-    public void onMouseDown(final TMouseEvent mouse) {
-        if ((mouseOnCheckbox(mouse)) && (mouse.isMouse1())) {
-            // Switch state
-            checked = !checked;
-        }
+    public boolean isChecked() {
+        return checked;
     }
 
     /**
-     * Handle keystrokes.
+     * Set checked value.
      *
-     * @param keypress keystroke event
+     * @param checked new checked value.
      */
-    @Override
-    public void onKeypress(final TKeypressEvent keypress) {
-        if (keypress.equals(kbSpace)) {
-            checked = !checked;
-            return;
-        }
-
-        // Pass to parent for the things we don't care about.
-        super.onKeypress(keypress);
+    public void setChecked(final boolean checked) {
+        this.checked = checked;
     }
 
 }
index a814fae7c15d79ee2f201d547be83e32e95a3382..b6a0411c099018cf78d69690c8e5041c7c41a0fc 100644 (file)
@@ -35,6 +35,10 @@ package jexer;
  */
 public class TCommand {
 
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Immediately abort the application (e.g. remote side closed
      * connection).
@@ -131,11 +135,40 @@ public class TCommand {
      */
     public static final int SAVE                = 30;
 
+    public static final TCommand cmAbort        = new TCommand(ABORT);
+    public static final TCommand cmExit         = new TCommand(EXIT);
+    public static final TCommand cmQuit         = new TCommand(EXIT);
+    public static final TCommand cmOpen         = new TCommand(OPEN);
+    public static final TCommand cmShell        = new TCommand(SHELL);
+    public static final TCommand cmCut          = new TCommand(CUT);
+    public static final TCommand cmCopy         = new TCommand(COPY);
+    public static final TCommand cmPaste        = new TCommand(PASTE);
+    public static final TCommand cmClear        = new TCommand(CLEAR);
+    public static final TCommand cmTile         = new TCommand(TILE);
+    public static final TCommand cmCascade      = new TCommand(CASCADE);
+    public static final TCommand cmCloseAll     = new TCommand(CLOSE_ALL);
+    public static final TCommand cmWindowMove   = new TCommand(WINDOW_MOVE);
+    public static final TCommand cmWindowZoom   = new TCommand(WINDOW_ZOOM);
+    public static final TCommand cmWindowNext   = new TCommand(WINDOW_NEXT);
+    public static final TCommand cmWindowPrevious = new TCommand(WINDOW_PREVIOUS);
+    public static final TCommand cmWindowClose  = new TCommand(WINDOW_CLOSE);
+    public static final TCommand cmHelp         = new TCommand(HELP);
+    public static final TCommand cmSave         = new TCommand(SAVE);
+    public static final TCommand cmMenu         = new TCommand(MENU);
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Type of command, one of EXIT, CASCADE, etc.
      */
     private int type;
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Protected constructor.  Subclasses can be used to define new commands.
      *
@@ -145,6 +178,10 @@ public class TCommand {
         this.type = type;
     }
 
+    // ------------------------------------------------------------------------
+    // TCommand ---------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Make human-readable description of this TCommand.
      *
@@ -181,25 +218,4 @@ public class TCommand {
         return type;
     }
 
-    public static final TCommand cmAbort        = new TCommand(ABORT);
-    public static final TCommand cmExit         = new TCommand(EXIT);
-    public static final TCommand cmQuit         = new TCommand(EXIT);
-    public static final TCommand cmOpen         = new TCommand(OPEN);
-    public static final TCommand cmShell        = new TCommand(SHELL);
-    public static final TCommand cmCut          = new TCommand(CUT);
-    public static final TCommand cmCopy         = new TCommand(COPY);
-    public static final TCommand cmPaste        = new TCommand(PASTE);
-    public static final TCommand cmClear        = new TCommand(CLEAR);
-    public static final TCommand cmTile         = new TCommand(TILE);
-    public static final TCommand cmCascade      = new TCommand(CASCADE);
-    public static final TCommand cmCloseAll     = new TCommand(CLOSE_ALL);
-    public static final TCommand cmWindowMove   = new TCommand(WINDOW_MOVE);
-    public static final TCommand cmWindowZoom   = new TCommand(WINDOW_ZOOM);
-    public static final TCommand cmWindowNext   = new TCommand(WINDOW_NEXT);
-    public static final TCommand cmWindowPrevious = new TCommand(WINDOW_PREVIOUS);
-    public static final TCommand cmWindowClose  = new TCommand(WINDOW_CLOSE);
-    public static final TCommand cmHelp         = new TCommand(HELP);
-    public static final TCommand cmSave         = new TCommand(SAVE);
-    public static final TCommand cmMenu         = new TCommand(MENU);
-
 }
index 5b4abf39f467a80ecfaf4203cdfcb5bfd53cbffe..bc3712b7890b3d8d46be563b01d4cfb8bd172fa8 100644 (file)
@@ -51,6 +51,30 @@ public class TEditColorThemeWindow extends TWindow {
      */
     private static final ResourceBundle i18n = ResourceBundle.getBundle(TEditColorThemeWindow.class.getName());
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The current editing theme.
+     */
+    private ColorTheme editTheme;
+
+    /**
+     * The left-side list of colors pane.
+     */
+    private TList colorNames;
+
+    /**
+     * The foreground color.
+     */
+    private ForegroundPicker foreground;
+
+    /**
+     * The background color.
+     */
+    private BackgroundPicker background;
+
     /**
      * The foreground color picker.
      */
@@ -602,53 +626,9 @@ public class TEditColorThemeWindow extends TWindow {
 
     }
 
-    /**
-     * The current editing theme.
-     */
-    private ColorTheme editTheme;
-
-    /**
-     * The left-side list of colors pane.
-     */
-    private TList colorNames;
-
-    /**
-     * The foreground color.
-     */
-    private ForegroundPicker foreground;
-
-    /**
-     * The background color.
-     */
-    private BackgroundPicker background;
-
-    /**
-     * Set various widgets/values to the editing theme color.
-     *
-     * @param colorName name of color from theme
-     */
-    private void refreshFromTheme(final String colorName) {
-        CellAttributes attr = editTheme.getColor(colorName);
-        foreground.color = attr.getForeColor();
-        foreground.bold = attr.isBold();
-        background.color = attr.getBackColor();
-    }
-
-    /**
-     * Examines foreground, background, and colorNames and sets the color in
-     * editTheme.
-     */
-    private void saveToEditTheme() {
-        String colorName = colorNames.getSelected();
-        if (colorName == null) {
-            return;
-        }
-        CellAttributes attr = editTheme.getColor(colorName);
-        attr.setForeColor(foreground.color);
-        attr.setBold(foreground.bold);
-        attr.setBackColor(background.color);
-        editTheme.setColor(colorName, attr);
-    }
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Public constructor.  The file open box will be centered on screen.
@@ -720,6 +700,31 @@ public class TEditColorThemeWindow extends TWindow {
         newStatusBar(i18n.getString("statusBar"));
     }
 
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Handle keystrokes.
+     *
+     * @param keypress keystroke event
+     */
+    @Override
+    public void onKeypress(final TKeypressEvent keypress) {
+        // Escape - behave like cancel
+        if (keypress.equals(kbEsc)) {
+            getApplication().closeWindow(this);
+            return;
+        }
+
+        // Pass to my parent
+        super.onKeypress(keypress);
+    }
+
+    // ------------------------------------------------------------------------
+    // TWindow ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Draw me on screen.
      */
@@ -747,21 +752,36 @@ public class TEditColorThemeWindow extends TWindow {
             i18n.getString("textTextText"), attr);
     }
 
+    // ------------------------------------------------------------------------
+    // TEditColorThemeWindow --------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
-     * Handle keystrokes.
+     * Set various widgets/values to the editing theme color.
      *
-     * @param keypress keystroke event
+     * @param colorName name of color from theme
      */
-    @Override
-    public void onKeypress(final TKeypressEvent keypress) {
-        // Escape - behave like cancel
-        if (keypress.equals(kbEsc)) {
-            getApplication().closeWindow(this);
+    private void refreshFromTheme(final String colorName) {
+        CellAttributes attr = editTheme.getColor(colorName);
+        foreground.color = attr.getForeColor();
+        foreground.bold = attr.isBold();
+        background.color = attr.getBackColor();
+    }
+
+    /**
+     * Examines foreground, background, and colorNames and sets the color in
+     * editTheme.
+     */
+    private void saveToEditTheme() {
+        String colorName = colorNames.getSelected();
+        if (colorName == null) {
             return;
         }
-
-        // Pass to my parent
-        super.onKeypress(keypress);
+        CellAttributes attr = editTheme.getColor(colorName);
+        attr.setForeColor(foreground.color);
+        attr.setBold(foreground.bold);
+        attr.setBackColor(background.color);
+        editTheme.setColor(colorName, attr);
     }
 
 }
index c93cecbcbf97670babe1d07d26508498467a3030..8f693f2123539abfc8b4ca85ba923441cfada52c 100644 (file)
@@ -39,31 +39,15 @@ import static jexer.TKeypress.*;
  */
 public class TField extends TWidget {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Field text.
      */
     protected String text = "";
 
-    /**
-     * Get field text.
-     *
-     * @return field text
-     */
-    public final String getText() {
-        return text;
-    }
-
-    /**
-     * Set field text.
-     *
-     * @param text the new field text
-     */
-    public final void setText(String text) {
-        this.text = text;
-        position = 0;
-        windowStart = 0;
-    }
-
     /**
      * If true, only allow enough characters that will fit in the width.  If
      * false, allow the field to scroll to the right.
@@ -100,6 +84,10 @@ public class TField extends TWidget {
      */
     protected TAction updateAction;
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Public constructor.
      *
@@ -157,6 +145,10 @@ public class TField extends TWidget {
         this.updateAction = updateAction;
     }
 
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Returns true if the mouse is currently on the field.
      *
@@ -174,80 +166,6 @@ public class TField extends TWidget {
         return false;
     }
 
-    /**
-     * Dispatch to the action function.
-     *
-     * @param enter if true, the user pressed Enter, else this was an update
-     * to the text.
-     */
-    protected void dispatch(final boolean enter) {
-        if (enter) {
-            if (enterAction != null) {
-                enterAction.DO();
-            }
-        } else {
-            if (updateAction != null) {
-                updateAction.DO();
-            }
-        }
-    }
-
-    /**
-     * Draw the text field.
-     */
-    @Override
-    public void draw() {
-        CellAttributes fieldColor;
-
-        if (isAbsoluteActive()) {
-            fieldColor = getTheme().getColor("tfield.active");
-        } else {
-            fieldColor = getTheme().getColor("tfield.inactive");
-        }
-
-        int end = windowStart + getWidth();
-        if (end > text.length()) {
-            end = text.length();
-        }
-        getScreen().hLineXY(0, 0, getWidth(), GraphicsChars.HATCH, fieldColor);
-        getScreen().putStringXY(0, 0, text.substring(windowStart, end),
-            fieldColor);
-
-        // Fix the cursor, it will be rendered by TApplication.drawAll().
-        updateCursor();
-    }
-
-    /**
-     * Update the visible cursor position to match the location of position
-     * and windowStart.
-     */
-    protected void updateCursor() {
-        if ((position > getWidth()) && fixed) {
-            setCursorX(getWidth());
-        } else if ((position - windowStart == getWidth()) && !fixed) {
-            setCursorX(getWidth() - 1);
-        } else {
-            setCursorX(position - windowStart);
-        }
-    }
-
-    /**
-     * Normalize windowStart such that most of the field data if visible.
-     */
-    protected void normalizeWindowStart() {
-        if (fixed) {
-            // windowStart had better be zero, there is nothing to do here.
-            assert (windowStart == 0);
-            return;
-        }
-        windowStart = position - (getWidth() - 1);
-        if (windowStart < 0) {
-            windowStart = 0;
-        }
-
-        updateCursor();
-    }
-
     /**
      * Handle mouse button presses.
      *
@@ -419,6 +337,108 @@ public class TField extends TWidget {
         super.onKeypress(keypress);
     }
 
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw the text field.
+     */
+    @Override
+    public void draw() {
+        CellAttributes fieldColor;
+
+        if (isAbsoluteActive()) {
+            fieldColor = getTheme().getColor("tfield.active");
+        } else {
+            fieldColor = getTheme().getColor("tfield.inactive");
+        }
+
+        int end = windowStart + getWidth();
+        if (end > text.length()) {
+            end = text.length();
+        }
+        getScreen().hLineXY(0, 0, getWidth(), GraphicsChars.HATCH, fieldColor);
+        getScreen().putStringXY(0, 0, text.substring(windowStart, end),
+            fieldColor);
+
+        // Fix the cursor, it will be rendered by TApplication.drawAll().
+        updateCursor();
+    }
+
+    // ------------------------------------------------------------------------
+    // TField -----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get field text.
+     *
+     * @return field text
+     */
+    public final String getText() {
+        return text;
+    }
+
+    /**
+     * Set field text.
+     *
+     * @param text the new field text
+     */
+    public final void setText(String text) {
+        this.text = text;
+        position = 0;
+        windowStart = 0;
+    }
+
+    /**
+     * Dispatch to the action function.
+     *
+     * @param enter if true, the user pressed Enter, else this was an update
+     * to the text.
+     */
+    protected void dispatch(final boolean enter) {
+        if (enter) {
+            if (enterAction != null) {
+                enterAction.DO();
+            }
+        } else {
+            if (updateAction != null) {
+                updateAction.DO();
+            }
+        }
+    }
+
+    /**
+     * Update the visible cursor position to match the location of position
+     * and windowStart.
+     */
+    protected void updateCursor() {
+        if ((position > getWidth()) && fixed) {
+            setCursorX(getWidth());
+        } else if ((position - windowStart == getWidth()) && !fixed) {
+            setCursorX(getWidth() - 1);
+        } else {
+            setCursorX(position - windowStart);
+        }
+    }
+
+    /**
+     * Normalize windowStart such that most of the field data if visible.
+     */
+    protected void normalizeWindowStart() {
+        if (fixed) {
+            // windowStart had better be zero, there is nothing to do here.
+            assert (windowStart == 0);
+            return;
+        }
+        windowStart = position - (getWidth() - 1);
+        if (windowStart < 0) {
+            windowStart = 0;
+        }
+
+        updateCursor();
+    }
+
     /**
      * Append char to the end of the field.
      *
index 05629cf8e3d3cecf4a10a7a222a247ce6cbf22e4..18d65f22ea047b83cb17b709df4e7a71cc5e173d 100644 (file)
@@ -34,6 +34,9 @@ import java.util.ResourceBundle;
 
 import jexer.bits.GraphicsChars;
 import jexer.event.TKeypressEvent;
+import jexer.ttree.TDirectoryTreeItem;
+import jexer.ttree.TTreeItem;
+import jexer.ttree.TTreeViewWidget;
 import static jexer.TKeypress.*;
 
 /**
@@ -59,6 +62,10 @@ public final class TFileOpenBox extends TWindow {
      */
     private static final ResourceBundle i18n = ResourceBundle.getBundle(TFileOpenBox.class.getName());
 
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * TFileOpenBox can be called for either Open or Save actions.
      */
@@ -74,24 +81,19 @@ public final class TFileOpenBox extends TWindow {
         SAVE
     }
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * String to return, or null if the user canceled.
      */
     private String filename = null;
 
-    /**
-     * Get the return string.
-     *
-     * @return the filename the user selected, or null if they canceled.
-     */
-    public String getFilename() {
-        return filename;
-    }
-
     /**
      * The left-side tree view pane.
      */
-    private TTreeView treeView;
+    private TTreeViewWidget treeView;
 
     /**
      * The data behind treeView.
@@ -113,31 +115,9 @@ public final class TFileOpenBox extends TWindow {
      */
     private TButton openButton;
 
-    /**
-     * See if there is a valid filename to return.  If the filename is a
-     * directory, then
-     *
-     * @param newFilename the filename to check and return
-     * @throws IOException of a java.io operation throws
-     */
-    private void checkFilename(final String newFilename) throws IOException {
-        File newFile = new File(newFilename);
-        if (newFile.exists()) {
-            if (newFile.isFile()) {
-                filename = newFilename;
-                getApplication().closeWindow(this);
-                return;
-            }
-            if (newFile.isDirectory()) {
-                treeViewRoot = new TDirectoryTreeItem(treeView, newFilename,
-                    true);
-                treeView.setTreeRoot(treeViewRoot, true);
-                treeView.reflowData();
-                openButton.setEnabled(false);
-                directoryList.setPath(newFilename);
-            }
-        }
-    }
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Public constructor.  The file open box will be centered on screen.
@@ -168,7 +148,7 @@ public final class TFileOpenBox extends TWindow {
         entryField.onKeypress(new TKeypressEvent(kbEnd));
 
         // Add directory treeView
-        treeView = addTreeView(1, 3, 30, getHeight() - 6,
+        treeView = addTreeViewWidget(1, 3, 30, getHeight() - 6,
             new TAction() {
                 public void DO() {
                     TTreeItem item = treeView.getSelected();
@@ -176,7 +156,7 @@ public final class TFileOpenBox extends TWindow {
                     try {
                         directoryList.setPath(selectedDir.getCanonicalPath());
                         openButton.setEnabled(false);
-                        activate(directoryList);
+                        activate(treeView);
                     } catch (IOException e) {
                         e.printStackTrace();
                     }
@@ -250,15 +230,9 @@ public final class TFileOpenBox extends TWindow {
         getApplication().yield();
     }
 
-    /**
-     * Draw me on screen.
-     */
-    @Override
-    public void draw() {
-        super.draw();
-        getScreen().vLineXY(33, 4, getHeight() - 6, GraphicsChars.WINDOW_SIDE,
-            getBackground());
-    }
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Handle keystrokes.
@@ -275,8 +249,86 @@ public final class TFileOpenBox extends TWindow {
             return;
         }
 
+        if (treeView.isActive()) {
+            if ((keypress.equals(kbEnter))
+                || (keypress.equals(kbUp))
+                || (keypress.equals(kbDown))
+                || (keypress.equals(kbPgUp))
+                || (keypress.equals(kbPgDn))
+                || (keypress.equals(kbHome))
+                || (keypress.equals(kbEnd))
+            ) {
+                // Tree view will be changing, update the directory list.
+                super.onKeypress(keypress);
+
+                // This is the same action as treeView's enter.
+                TTreeItem item = treeView.getSelected();
+                File selectedDir = ((TDirectoryTreeItem) item).getFile();
+                try {
+                    directoryList.setPath(selectedDir.getCanonicalPath());
+                    openButton.setEnabled(false);
+                    activate(treeView);
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+                return;
+            }
+        }
+
         // Pass to my parent
         super.onKeypress(keypress);
     }
 
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw me on screen.
+     */
+    @Override
+    public void draw() {
+        super.draw();
+        getScreen().vLineXY(33, 4, getHeight() - 6, GraphicsChars.WINDOW_SIDE,
+            getBackground());
+    }
+
+    // ------------------------------------------------------------------------
+    // TFileOpenBox -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the return string.
+     *
+     * @return the filename the user selected, or null if they canceled.
+     */
+    public String getFilename() {
+        return filename;
+    }
+
+    /**
+     * See if there is a valid filename to return.  If the filename is a
+     * directory, then
+     *
+     * @param newFilename the filename to check and return
+     * @throws IOException of a java.io operation throws
+     */
+    private void checkFilename(final String newFilename) throws IOException {
+        File newFile = new File(newFilename);
+        if (newFile.exists()) {
+            if (newFile.isFile()) {
+                filename = newFilename;
+                getApplication().closeWindow(this);
+                return;
+            }
+            if (newFile.isDirectory()) {
+                treeViewRoot = new TDirectoryTreeItem(treeView,
+                    newFilename, true);
+                treeView.setTreeRoot(treeViewRoot, true);
+                openButton.setEnabled(false);
+                directoryList.setPath(newFilename);
+            }
+        }
+    }
+
 }
index 9e9b372ec51854d8bf75e968746d65e58ef1c8c8..90133a46a0b89049664d687e109f404af8753ab2 100644 (file)
@@ -37,11 +37,206 @@ import jexer.event.TMouseEvent;
  */
 public final class THScroller extends TWidget {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Value that corresponds to being on the left edge of the scroll bar.
      */
     private int leftValue = 0;
 
+    /**
+     * Value that corresponds to being on the right edge of the scroll bar.
+     */
+    private int rightValue = 100;
+
+    /**
+     * Current value of the scroll.
+     */
+    private int value = 0;
+
+    /**
+     * The increment for clicking on an arrow.
+     */
+    private int smallChange = 1;
+
+    /**
+     * The increment for clicking in the bar between the box and an arrow.
+     */
+    private int bigChange = 20;
+
+    /**
+     * When true, the user is dragging the scroll box.
+     */
+    private boolean inScroll = false;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width height of scroll bar
+     */
+    public THScroller(final TWidget parent, final int x, final int y,
+        final int width) {
+
+        // Set parent and window
+        super(parent, x, y, width, 1);
+    }
+
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Handle mouse button releases.
+     *
+     * @param mouse mouse button release event
+     */
+    @Override
+    public void onMouseUp(final TMouseEvent mouse) {
+
+        if (inScroll) {
+            inScroll = false;
+            return;
+        }
+
+        if (rightValue == leftValue) {
+            return;
+        }
+
+        if ((mouse.getX() == 0)
+            && (mouse.getY() == 0)
+        ) {
+            // Clicked on the left arrow
+            decrement();
+            return;
+        }
+
+        if ((mouse.getY() == 0)
+            && (mouse.getX() == getWidth() - 1)
+        ) {
+            // Clicked on the right arrow
+            increment();
+            return;
+        }
+
+        if ((mouse.getY() == 0)
+            && (mouse.getX() > 0)
+            && (mouse.getX() < boxPosition())
+        ) {
+            // Clicked between the left arrow and the box
+            value -= bigChange;
+            if (value < leftValue) {
+                value = leftValue;
+            }
+            return;
+        }
+
+        if ((mouse.getY() == 0)
+            && (mouse.getX() > boxPosition())
+            && (mouse.getX() < getWidth() - 1)
+        ) {
+            // Clicked between the box and the right arrow
+            value += bigChange;
+            if (value > rightValue) {
+                value = rightValue;
+            }
+            return;
+        }
+    }
+
+    /**
+     * Handle mouse movement events.
+     *
+     * @param mouse mouse motion event
+     */
+    @Override
+    public void onMouseMotion(final TMouseEvent mouse) {
+
+        if (rightValue == leftValue) {
+            inScroll = false;
+            return;
+        }
+
+        if ((mouse.isMouse1())
+            && (inScroll)
+            && (mouse.getX() > 0)
+            && (mouse.getX() < getWidth() - 1)
+        ) {
+            // Recompute value based on new box position
+            value = (rightValue - leftValue)
+                * (mouse.getX()) / (getWidth() - 3) + leftValue;
+            if (value > rightValue) {
+                value = rightValue;
+            }
+            if (value < leftValue) {
+                value = leftValue;
+            }
+            return;
+        }
+        inScroll = false;
+    }
+
+    /**
+     * Handle mouse button press events.
+     *
+     * @param mouse mouse button press event
+     */
+    @Override
+    public void onMouseDown(final TMouseEvent mouse) {
+        if (rightValue == leftValue) {
+            inScroll = false;
+            return;
+        }
+
+        if ((mouse.getY() == 0)
+            && (mouse.getX() == boxPosition())
+        ) {
+            inScroll = true;
+            return;
+        }
+
+    }
+
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw a horizontal scroll bar.
+     */
+    @Override
+    public void draw() {
+        CellAttributes arrowColor = getTheme().getColor("tscroller.arrows");
+        CellAttributes barColor = getTheme().getColor("tscroller.bar");
+        getScreen().putCharXY(0, 0, GraphicsChars.CP437[0x11], arrowColor);
+        getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0x10],
+            arrowColor);
+
+        // Place the box
+        if (rightValue > leftValue) {
+            getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.CP437[0xB1],
+                barColor);
+            getScreen().putCharXY(boxPosition(), 0, GraphicsChars.BOX,
+                arrowColor);
+        } else {
+            getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.HATCH,
+                barColor);
+        }
+
+    }
+
+    // ------------------------------------------------------------------------
+    // THScroller -------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Get the value that corresponds to being on the left edge of the scroll
      * bar.
@@ -62,11 +257,6 @@ public final class THScroller extends TWidget {
         this.leftValue = leftValue;
     }
 
-    /**
-     * Value that corresponds to being on the right edge of the scroll bar.
-     */
-    private int rightValue = 100;
-
     /**
      * Get the value that corresponds to being on the right edge of the
      * scroll bar.
@@ -87,11 +277,6 @@ public final class THScroller extends TWidget {
         this.rightValue = rightValue;
     }
 
-    /**
-     * Current value of the scroll.
-     */
-    private int value = 0;
-
     /**
      * Get current value of the scroll.
      *
@@ -110,11 +295,6 @@ public final class THScroller extends TWidget {
         this.value = value;
     }
 
-    /**
-     * The increment for clicking on an arrow.
-     */
-    private int smallChange = 1;
-
     /**
      * Get the increment for clicking on an arrow.
      *
@@ -133,11 +313,6 @@ public final class THScroller extends TWidget {
         this.smallChange = smallChange;
     }
 
-    /**
-     * The increment for clicking in the bar between the box and an arrow.
-     */
-    private int bigChange = 20;
-
     /**
      * Set the increment for clicking in the bar between the box and an
      * arrow.
@@ -158,26 +333,6 @@ public final class THScroller extends TWidget {
         this.bigChange = bigChange;
     }
 
-    /**
-     * When true, the user is dragging the scroll box.
-     */
-    private boolean inScroll = false;
-
-    /**
-     * Public constructor.
-     *
-     * @param parent parent widget
-     * @param x column relative to parent
-     * @param y row relative to parent
-     * @param width height of scroll bar
-     */
-    public THScroller(final TWidget parent, final int x, final int y,
-        final int width) {
-
-        // Set parent and window
-        super(parent, x, y, width, 1);
-    }
-
     /**
      * Compute the position of the scroll box (a.k.a. grip, thumb).
      *
@@ -187,30 +342,6 @@ public final class THScroller extends TWidget {
         return (getWidth() - 3) * (value - leftValue) / (rightValue - leftValue) + 1;
     }
 
-    /**
-     * Draw a horizontal scroll bar.
-     */
-    @Override
-    public void draw() {
-        CellAttributes arrowColor = getTheme().getColor("tscroller.arrows");
-        CellAttributes barColor = getTheme().getColor("tscroller.bar");
-        getScreen().putCharXY(0, 0, GraphicsChars.CP437[0x11], arrowColor);
-        getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0x10],
-            arrowColor);
-
-        // Place the box
-        if (rightValue > leftValue) {
-            getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.CP437[0xB1],
-                barColor);
-            getScreen().putCharXY(boxPosition(), 0, GraphicsChars.BOX,
-                arrowColor);
-        } else {
-            getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.HATCH,
-                barColor);
-        }
-
-    }
-
     /**
      * Perform a small step change left.
      */
@@ -277,109 +408,4 @@ public final class THScroller extends TWidget {
         value = rightValue;
     }
 
-    /**
-     * Handle mouse button releases.
-     *
-     * @param mouse mouse button release event
-     */
-    @Override
-    public void onMouseUp(final TMouseEvent mouse) {
-
-        if (inScroll) {
-            inScroll = false;
-            return;
-        }
-
-        if (rightValue == leftValue) {
-            return;
-        }
-
-        if ((mouse.getX() == 0)
-            && (mouse.getY() == 0)
-        ) {
-            // Clicked on the left arrow
-            decrement();
-            return;
-        }
-
-        if ((mouse.getY() == 0)
-            && (mouse.getX() == getWidth() - 1)
-        ) {
-            // Clicked on the right arrow
-            increment();
-            return;
-        }
-
-        if ((mouse.getY() == 0)
-            && (mouse.getX() > 0)
-            && (mouse.getX() < boxPosition())
-        ) {
-            // Clicked between the left arrow and the box
-            value -= bigChange;
-            if (value < leftValue) {
-                value = leftValue;
-            }
-            return;
-        }
-
-        if ((mouse.getY() == 0)
-            && (mouse.getX() > boxPosition())
-            && (mouse.getX() < getWidth() - 1)
-        ) {
-            // Clicked between the box and the right arrow
-            value += bigChange;
-            if (value > rightValue) {
-                value = rightValue;
-            }
-            return;
-        }
-    }
-
-    /**
-     * Handle mouse movement events.
-     *
-     * @param mouse mouse motion event
-     */
-    @Override
-    public void onMouseMotion(final TMouseEvent mouse) {
-
-        if (rightValue == leftValue) {
-            inScroll = false;
-            return;
-        }
-
-        if ((mouse.isMouse1())
-            && (inScroll)
-            && (mouse.getX() > 0)
-            && (mouse.getX() < getWidth() - 1)
-        ) {
-            // Recompute value based on new box position
-            value = (rightValue - leftValue)
-                * (mouse.getX()) / (getWidth() - 3) + leftValue;
-            return;
-        }
-        inScroll = false;
-    }
-
-    /**
-     * Handle mouse button press events.
-     *
-     * @param mouse mouse button press event
-     */
-    @Override
-    public void onMouseDown(final TMouseEvent mouse) {
-        if (rightValue == leftValue) {
-            inScroll = false;
-            return;
-        }
-
-        if ((mouse.getY() == 0)
-            && (mouse.getX() == boxPosition())
-        ) {
-            inScroll = true;
-            return;
-        }
-
-    }
-
 }
index 872ebc45b50d44891267aaa265994e6b4538bf07..2470bdfba892196348016f85f0633489413352c5 100644 (file)
@@ -33,6 +33,10 @@ package jexer;
  */
 public final class TKeypress {
 
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     // Various special keystrokes
 
     /**
@@ -170,386 +174,10 @@ public final class TKeypress {
      */
     public static final int ESC         = 43;
 
-    /**
-     * If true, ch is meaningless, use keyCode instead.
-     */
-    private boolean isFunctionKey;
-
-    /**
-     * Getter for isFunctionKey.
-     *
-     * @return if true, ch is meaningless, use keyCode instead
-     */
-    public boolean isFnKey() {
-        return isFunctionKey;
-    }
-
-    /**
-     * Will be set to F1, F2, HOME, END, etc. if isKey is true.
-     */
-    private int keyCode;
-
-    /**
-     * Getter for function key code.
-     *
-     * @return function key code int value (only valid is isKey is true)
-     */
-    public int getKeyCode() {
-        return keyCode;
-    }
-
-    /**
-     * Keystroke modifier ALT.
-     */
-    private boolean alt;
-
-    /**
-     * Getter for ALT.
-     *
-     * @return alt value
-     */
-    public boolean isAlt() {
-        return alt;
-    }
-
-    /**
-     * Keystroke modifier CTRL.
-     */
-    private boolean ctrl;
-
-    /**
-     * Getter for CTRL.
-     *
-     * @return ctrl value
-     */
-    public boolean isCtrl() {
-        return ctrl;
-    }
-
-    /**
-     * Keystroke modifier SHIFT.
-     */
-    private boolean shift;
-
-    /**
-     * Getter for SHIFT.
-     *
-     * @return shift value
-     */
-    public boolean isShift() {
-        return shift;
-    }
-
-    /**
-     * The character received.
-     */
-    private char ch;
-
-    /**
-     * Getter for character.
-     *
-     * @return the character (only valid if isKey is false)
-     */
-    public char getChar() {
-        return ch;
-    }
-
-    /**
-     * Public constructor makes an immutable instance.
-     *
-     * @param isKey is true, this is a function key
-     * @param fnKey the function key code (only valid if isKey is true)
-     * @param ch the character (only valid if fnKey is false)
-     * @param alt if true, ALT was pressed with this keystroke
-     * @param ctrl if true, CTRL was pressed with this keystroke
-     * @param shift if true, SHIFT was pressed with this keystroke
-     */
-    public TKeypress(final boolean isKey, final int fnKey, final char ch,
-            final boolean alt, final boolean ctrl, final boolean shift) {
-
-        this.isFunctionKey = isKey;
-        this.keyCode       = fnKey;
-        this.ch            = ch;
-        this.alt           = alt;
-        this.ctrl          = ctrl;
-        this.shift         = shift;
-    }
-
-    /**
-     * Create a duplicate instance.
-     *
-     * @return duplicate intance
-     */
-    public TKeypress dup() {
-        TKeypress keypress = new TKeypress(isFunctionKey, keyCode, ch,
-            alt, ctrl, shift);
-        return keypress;
-    }
-
-    /**
-     * Comparison check.  All fields must match to return true.
-     *
-     * @param rhs another TKeypress instance
-     * @return true if all fields are equal
-     */
-    @Override
-    public boolean equals(final Object rhs) {
-        if (!(rhs instanceof TKeypress)) {
-            return false;
-        }
-
-        TKeypress that = (TKeypress) rhs;
-        return ((isFunctionKey == that.isFunctionKey)
-                && (keyCode == that.keyCode)
-                && (ch == that.ch)
-                && (alt == that.alt)
-                && (ctrl == that.ctrl)
-                && (shift == that.shift));
-    }
-
-    /**
-     * Comparison check, omitting the ctrl/alt/shift flags.
-     *
-     * @param rhs another TKeypress instance
-     * @return true if all fields (except for ctrl/alt/shift) are equal
-     */
-    public boolean equalsWithoutModifiers(final Object rhs) {
-        if (!(rhs instanceof TKeypress)) {
-            return false;
-        }
-
-        TKeypress that = (TKeypress) rhs;
-        return ((isFunctionKey == that.isFunctionKey)
-                && (keyCode == that.keyCode)
-                && (ch == that.ch));
-    }
 
-    /**
-     * Hashcode uses all fields in equals().
-     *
-     * @return the hash
-     */
-    @Override
-    public int hashCode() {
-        int A = 13;
-        int B = 23;
-        int hash = A;
-        hash = (B * hash) + (isFunctionKey ? 1 : 0);
-        hash = (B * hash) + keyCode;
-        hash = (B * hash) + ch;
-        hash = (B * hash) + (alt ? 1 : 0);
-        hash = (B * hash) + (ctrl ? 1 : 0);
-        hash = (B * hash) + (shift ? 1 : 0);
-        return hash;
-    }
-
-    /**
-     * Make human-readable description of this TKeypress.
-     *
-     * @return displayable String
-     */
-    @Override
-    public String toString() {
-        if (isFunctionKey) {
-            switch (keyCode) {
-            case F1:
-                return String.format("%s%s%sF1",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case F2:
-                return String.format("%s%s%sF2",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case F3:
-                return String.format("%s%s%sF3",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case F4:
-                return String.format("%s%s%sF4",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case F5:
-                return String.format("%s%s%sF5",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case F6:
-                return String.format("%s%s%sF6",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case F7:
-                return String.format("%s%s%sF7",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case F8:
-                return String.format("%s%s%sF8",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case F9:
-                return String.format("%s%s%sF9",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case F10:
-                return String.format("%s%s%sF10",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case F11:
-                return String.format("%s%s%sF11",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case F12:
-                return String.format("%s%s%sF12",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case HOME:
-                return String.format("%s%s%sHOME",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case END:
-                return String.format("%s%s%sEND",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case PGUP:
-                return String.format("%s%s%sPGUP",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case PGDN:
-                return String.format("%s%s%sPGDN",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case INS:
-                return String.format("%s%s%sINS",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case DEL:
-                return String.format("%s%s%sDEL",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case RIGHT:
-                return String.format("%s%s%sRIGHT",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case LEFT:
-                return String.format("%s%s%sLEFT",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case UP:
-                return String.format("%s%s%sUP",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case DOWN:
-                return String.format("%s%s%sDOWN",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case TAB:
-                return String.format("%s%s%sTAB",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case BTAB:
-                return String.format("%s%s%sBTAB",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case ENTER:
-                return String.format("%s%s%sENTER",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            case ESC:
-                return String.format("%s%s%sESC",
-                        ctrl ? "Ctrl+" : "",
-                                alt ? "Alt+" : "",
-                                        shift ? "Shift+" : "");
-            default:
-                return String.format("--UNKNOWN--");
-            }
-        } else {
-            if (alt && !shift && !ctrl) {
-                // Alt-X
-                return String.format("Alt+%c", Character.toUpperCase(ch));
-            } else if (!alt && shift && !ctrl) {
-                // Shift-X
-                return String.format("%c", ch);
-            } else if (!alt && !shift && ctrl) {
-                // Ctrl-X
-                return String.format("Ctrl+%c", ch);
-            } else if (alt && shift && !ctrl) {
-                // Alt-Shift-X
-                return String.format("Alt+Shift+%c", ch);
-            } else if (!alt && shift && ctrl) {
-                // Ctrl-Shift-X
-                return String.format("Ctrl+Shift+%c", ch);
-            } else if (alt && !shift && ctrl) {
-                // Ctrl-Alt-X
-                return String.format("Ctrl+Alt+%c", Character.toUpperCase(ch));
-            } else if (alt && shift && ctrl) {
-                // Ctrl-Alt-Shift-X
-                return String.format("Ctrl+Alt+Shift+%c",
-                        Character.toUpperCase(ch));
-            } else {
-                // X
-                return String.format("%c", ch);
-            }
-        }
-    }
-
-    /**
-     * Convert a keypress to lowercase.  Function keys and alt/ctrl keys are
-     * not converted.
-     *
-     * @return a new instance with the key converted
-     */
-    public TKeypress toLowerCase() {
-        TKeypress newKey = new TKeypress(isFunctionKey, keyCode, ch, alt, ctrl,
-                shift);
-        if (!isFunctionKey && (ch >= 'A') && (ch <= 'Z') && !ctrl && !alt) {
-            newKey.shift = false;
-            newKey.ch += 32;
-        }
-        return newKey;
-    }
-
-    /**
-     * Convert a keypress to uppercase.  Function keys and alt/ctrl keys are
-     * not converted.
-     *
-     * @return a new instance with the key converted
-     */
-    public TKeypress toUpperCase() {
-        TKeypress newKey = new TKeypress(isFunctionKey, keyCode, ch, alt, ctrl,
-                shift);
-        if (!isFunctionKey && (ch >= 'a') && (ch <= 'z') && !ctrl && !alt) {
-            newKey.shift = true;
-            newKey.ch -= 32;
-        }
-        return newKey;
-    }
-
-    // Special "no-key" keypress, used to ignore undefined keystrokes
-    public static final TKeypress kbNoKey = new TKeypress(true,
-            TKeypress.NONE, ' ', false, false, false);
+    // Special "no-key" keypress, used to ignore undefined keystrokes
+    public static final TKeypress kbNoKey = new TKeypress(true,
+            TKeypress.NONE, ' ', false, false, false);
 
     // Normal keys
     public static final TKeypress kbF1 = new TKeypress(true,
@@ -977,4 +605,393 @@ public final class TKeypress {
     public static final TKeypress kbBackspaceDel = new TKeypress(false,
             0, (char)0x7F, false, false, false);
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * If true, ch is meaningless, use keyCode instead.
+     */
+    private boolean isFunctionKey;
+
+    /**
+     * Will be set to F1, F2, HOME, END, etc. if isKey is true.
+     */
+    private int keyCode;
+
+    /**
+     * Keystroke modifier ALT.
+     */
+    private boolean alt;
+
+    /**
+     * Keystroke modifier CTRL.
+     */
+    private boolean ctrl;
+
+    /**
+     * Keystroke modifier SHIFT.
+     */
+    private boolean shift;
+
+    /**
+     * The character received.
+     */
+    private char ch;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor makes an immutable instance.
+     *
+     * @param isKey is true, this is a function key
+     * @param fnKey the function key code (only valid if isKey is true)
+     * @param ch the character (only valid if fnKey is false)
+     * @param alt if true, ALT was pressed with this keystroke
+     * @param ctrl if true, CTRL was pressed with this keystroke
+     * @param shift if true, SHIFT was pressed with this keystroke
+     */
+    public TKeypress(final boolean isKey, final int fnKey, final char ch,
+            final boolean alt, final boolean ctrl, final boolean shift) {
+
+        this.isFunctionKey = isKey;
+        this.keyCode       = fnKey;
+        this.ch            = ch;
+        this.alt           = alt;
+        this.ctrl          = ctrl;
+        this.shift         = shift;
+    }
+
+    // ------------------------------------------------------------------------
+    // TKeypress --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Getter for isFunctionKey.
+     *
+     * @return if true, ch is meaningless, use keyCode instead
+     */
+    public boolean isFnKey() {
+        return isFunctionKey;
+    }
+
+    /**
+     * Getter for function key code.
+     *
+     * @return function key code int value (only valid is isKey is true)
+     */
+    public int getKeyCode() {
+        return keyCode;
+    }
+
+    /**
+     * Getter for ALT.
+     *
+     * @return alt value
+     */
+    public boolean isAlt() {
+        return alt;
+    }
+
+    /**
+     * Getter for CTRL.
+     *
+     * @return ctrl value
+     */
+    public boolean isCtrl() {
+        return ctrl;
+    }
+
+    /**
+     * Getter for SHIFT.
+     *
+     * @return shift value
+     */
+    public boolean isShift() {
+        return shift;
+    }
+
+    /**
+     * Getter for character.
+     *
+     * @return the character (only valid if isKey is false)
+     */
+    public char getChar() {
+        return ch;
+    }
+
+    /**
+     * Create a duplicate instance.
+     *
+     * @return duplicate intance
+     */
+    public TKeypress dup() {
+        TKeypress keypress = new TKeypress(isFunctionKey, keyCode, ch,
+            alt, ctrl, shift);
+        return keypress;
+    }
+
+    /**
+     * Comparison check.  All fields must match to return true.
+     *
+     * @param rhs another TKeypress instance
+     * @return true if all fields are equal
+     */
+    @Override
+    public boolean equals(final Object rhs) {
+        if (!(rhs instanceof TKeypress)) {
+            return false;
+        }
+
+        TKeypress that = (TKeypress) rhs;
+        return ((isFunctionKey == that.isFunctionKey)
+                && (keyCode == that.keyCode)
+                && (ch == that.ch)
+                && (alt == that.alt)
+                && (ctrl == that.ctrl)
+                && (shift == that.shift));
+    }
+
+    /**
+     * Comparison check, omitting the ctrl/alt/shift flags.
+     *
+     * @param rhs another TKeypress instance
+     * @return true if all fields (except for ctrl/alt/shift) are equal
+     */
+    public boolean equalsWithoutModifiers(final Object rhs) {
+        if (!(rhs instanceof TKeypress)) {
+            return false;
+        }
+
+        TKeypress that = (TKeypress) rhs;
+        return ((isFunctionKey == that.isFunctionKey)
+                && (keyCode == that.keyCode)
+                && (ch == that.ch));
+    }
+
+    /**
+     * Hashcode uses all fields in equals().
+     *
+     * @return the hash
+     */
+    @Override
+    public int hashCode() {
+        int A = 13;
+        int B = 23;
+        int hash = A;
+        hash = (B * hash) + (isFunctionKey ? 1 : 0);
+        hash = (B * hash) + keyCode;
+        hash = (B * hash) + ch;
+        hash = (B * hash) + (alt ? 1 : 0);
+        hash = (B * hash) + (ctrl ? 1 : 0);
+        hash = (B * hash) + (shift ? 1 : 0);
+        return hash;
+    }
+
+    /**
+     * Make human-readable description of this TKeypress.
+     *
+     * @return displayable String
+     */
+    @Override
+    public String toString() {
+        if (isFunctionKey) {
+            switch (keyCode) {
+            case F1:
+                return String.format("%s%s%sF1",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case F2:
+                return String.format("%s%s%sF2",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case F3:
+                return String.format("%s%s%sF3",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case F4:
+                return String.format("%s%s%sF4",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case F5:
+                return String.format("%s%s%sF5",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case F6:
+                return String.format("%s%s%sF6",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case F7:
+                return String.format("%s%s%sF7",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case F8:
+                return String.format("%s%s%sF8",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case F9:
+                return String.format("%s%s%sF9",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case F10:
+                return String.format("%s%s%sF10",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case F11:
+                return String.format("%s%s%sF11",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case F12:
+                return String.format("%s%s%sF12",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case HOME:
+                return String.format("%s%s%sHOME",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case END:
+                return String.format("%s%s%sEND",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case PGUP:
+                return String.format("%s%s%sPGUP",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case PGDN:
+                return String.format("%s%s%sPGDN",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case INS:
+                return String.format("%s%s%sINS",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case DEL:
+                return String.format("%s%s%sDEL",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case RIGHT:
+                return String.format("%s%s%sRIGHT",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case LEFT:
+                return String.format("%s%s%sLEFT",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case UP:
+                return String.format("%s%s%sUP",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case DOWN:
+                return String.format("%s%s%sDOWN",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case TAB:
+                return String.format("%s%s%sTAB",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case BTAB:
+                return String.format("%s%s%sBTAB",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case ENTER:
+                return String.format("%s%s%sENTER",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            case ESC:
+                return String.format("%s%s%sESC",
+                        ctrl ? "Ctrl+" : "",
+                                alt ? "Alt+" : "",
+                                        shift ? "Shift+" : "");
+            default:
+                return String.format("--UNKNOWN--");
+            }
+        } else {
+            if (alt && !shift && !ctrl) {
+                // Alt-X
+                return String.format("Alt+%c", Character.toUpperCase(ch));
+            } else if (!alt && shift && !ctrl) {
+                // Shift-X
+                return String.format("%c", ch);
+            } else if (!alt && !shift && ctrl) {
+                // Ctrl-X
+                return String.format("Ctrl+%c", ch);
+            } else if (alt && shift && !ctrl) {
+                // Alt-Shift-X
+                return String.format("Alt+Shift+%c", ch);
+            } else if (!alt && shift && ctrl) {
+                // Ctrl-Shift-X
+                return String.format("Ctrl+Shift+%c", ch);
+            } else if (alt && !shift && ctrl) {
+                // Ctrl-Alt-X
+                return String.format("Ctrl+Alt+%c", Character.toUpperCase(ch));
+            } else if (alt && shift && ctrl) {
+                // Ctrl-Alt-Shift-X
+                return String.format("Ctrl+Alt+Shift+%c",
+                        Character.toUpperCase(ch));
+            } else {
+                // X
+                return String.format("%c", ch);
+            }
+        }
+    }
+
+    /**
+     * Convert a keypress to lowercase.  Function keys and alt/ctrl keys are
+     * not converted.
+     *
+     * @return a new instance with the key converted
+     */
+    public TKeypress toLowerCase() {
+        TKeypress newKey = new TKeypress(isFunctionKey, keyCode, ch, alt, ctrl,
+                shift);
+        if (!isFunctionKey && (ch >= 'A') && (ch <= 'Z') && !ctrl && !alt) {
+            newKey.shift = false;
+            newKey.ch += 32;
+        }
+        return newKey;
+    }
+
+    /**
+     * Convert a keypress to uppercase.  Function keys and alt/ctrl keys are
+     * not converted.
+     *
+     * @return a new instance with the key converted
+     */
+    public TKeypress toUpperCase() {
+        TKeypress newKey = new TKeypress(isFunctionKey, keyCode, ch, alt, ctrl,
+                shift);
+        if (!isFunctionKey && (ch >= 'a') && (ch <= 'z') && !ctrl && !alt) {
+            newKey.shift = true;
+            newKey.ch -= 32;
+        }
+        return newKey;
+    }
+
 }
index d5bb24cd3f180f2fe5985f6dce80ef56c4aff6c5..2eeea915793e0568db485f82cccc89efb9f2ae8a 100644 (file)
@@ -35,34 +35,24 @@ import jexer.bits.CellAttributes;
  */
 public final class TLabel extends TWidget {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Label text.
      */
     private String label = "";
 
-    /**
-     * Get label text.
-     *
-     * @return label text
-     */
-    public String getLabel() {
-        return label;
-    }
-
-    /**
-     * Set label text.
-     *
-     * @param label new label text
-     */
-    public void setLabel(final String label) {
-        this.label = label;
-    }
-
     /**
      * Label color.
      */
     private String colorKey;
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Public constructor, using the default "tlabel" for colorKey.
      *
@@ -96,6 +86,10 @@ public final class TLabel extends TWidget {
         this.colorKey = colorKey;
     }
 
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Draw a static label.
      */
@@ -110,4 +104,26 @@ public final class TLabel extends TWidget {
         getScreen().putStringXY(0, 0, label, color);
     }
 
+    // ------------------------------------------------------------------------
+    // TLabel -----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get label text.
+     *
+     * @return label text
+     */
+    public String getLabel() {
+        return label;
+    }
+
+    /**
+     * Set label text.
+     *
+     * @param label new label text
+     */
+    public void setLabel(final String label) {
+        this.label = label;
+    }
+
 }
index e47d7512e48cd175e480298daa6488824c84335b..46c9307b95eba5cf9f68ed59d82ed4893ca7f1f2 100644 (file)
@@ -41,6 +41,10 @@ import static jexer.TKeypress.*;
  */
 public class TList extends TScrollableWidget {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The list of strings to display.
      */
@@ -51,56 +55,6 @@ public class TList extends TScrollableWidget {
      */
     private int selectedString = -1;
 
-    /**
-     * Get the selection index.
-     *
-     * @return -1 if nothing is selected, otherwise the index into the list
-     */
-    public final int getSelectedIndex() {
-        return selectedString;
-    }
-
-    /**
-     * Set the selected string index.
-     *
-     * @param index -1 to unselect, otherwise the index into the list
-     */
-    public final void setSelectedIndex(final int index) {
-        selectedString = index;
-    }
-
-    /**
-     * Get the selected string.
-     *
-     * @return the selected string, or null of nothing is selected yet
-     */
-    public final String getSelected() {
-        if ((selectedString >= 0) && (selectedString <= strings.size() - 1)) {
-            return strings.get(selectedString);
-        }
-        return null;
-    }
-
-    /**
-     * Get the maximum selection index value.
-     *
-     * @return -1 if the list is empty
-     */
-    public final int getMaxSelectedIndex() {
-        return strings.size() - 1;
-    }
-
-    /**
-     * Set the new list of strings to display.
-     *
-     * @param list new list of strings
-     */
-    public final void setList(final List<String> list) {
-        strings.clear();
-        strings.addAll(list);
-        reflowData();
-    }
-
     /**
      * Maximum width of a single line.
      */
@@ -116,55 +70,9 @@ public class TList extends TScrollableWidget {
      */
     private TAction moveAction = null;
 
-    /**
-     * Perform user selection action.
-     */
-    public void dispatchEnter() {
-        assert (selectedString >= 0);
-        assert (selectedString < strings.size());
-        if (enterAction != null) {
-            enterAction.DO();
-        }
-    }
-
-    /**
-     * Perform list movement action.
-     */
-    public void dispatchMove() {
-        assert (selectedString >= 0);
-        assert (selectedString < strings.size());
-        if (moveAction != null) {
-            moveAction.DO();
-        }
-    }
-
-    /**
-     * Resize for a new width/height.
-     */
-    @Override
-    public void reflowData() {
-
-        // Reset the lines
-        selectedString = -1;
-        maxLineWidth = 0;
-
-        for (int i = 0; i < strings.size(); i++) {
-            String line = strings.get(i);
-            if (line.length() > maxLineWidth) {
-                maxLineWidth = line.length();
-            }
-        }
-
-        setBottomValue(strings.size() - getHeight() + 1);
-        if (getBottomValue() < 0) {
-            setBottomValue(0);
-        }
-
-        setRightValue(maxLineWidth - getWidth() + 1);
-        if (getRightValue() < 0) {
-            setRightValue(0);
-        }
-    }
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Public constructor.
@@ -241,48 +149,9 @@ public class TList extends TScrollableWidget {
         reflowData();
     }
 
-    /**
-     * Draw the files list.
-     */
-    @Override
-    public void draw() {
-        CellAttributes color = null;
-        int begin = getVerticalValue();
-        int topY = 0;
-        for (int i = begin; i < strings.size(); i++) {
-            String line = strings.get(i);
-            if (getHorizontalValue() < line.length()) {
-                line = line.substring(getHorizontalValue());
-            } else {
-                line = "";
-            }
-            if (i == selectedString) {
-                color = getTheme().getColor("tlist.selected");
-            } else if (isAbsoluteActive()) {
-                color = getTheme().getColor("tlist");
-            } else {
-                color = getTheme().getColor("tlist.inactive");
-            }
-            String formatString = "%-" + Integer.toString(getWidth() - 1) + "s";
-            getScreen().putStringXY(0, topY, String.format(formatString, line),
-                    color);
-            topY++;
-            if (topY >= getHeight() - 1) {
-                break;
-            }
-        }
-
-        if (isAbsoluteActive()) {
-            color = getTheme().getColor("tlist");
-        } else {
-            color = getTheme().getColor("tlist.inactive");
-        }
-
-        // Pad the rest with blank lines
-        for (int i = topY; i < getHeight() - 1; i++) {
-            getScreen().hLineXY(0, i, getWidth() - 1, ' ', color);
-        }
-    }
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Handle mouse press events.
@@ -427,4 +296,159 @@ public class TList extends TScrollableWidget {
         }
     }
 
+    // ------------------------------------------------------------------------
+    // TScrollableWidget ------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Resize for a new width/height.
+     */
+    @Override
+    public void reflowData() {
+
+        // Reset the lines
+        selectedString = -1;
+        maxLineWidth = 0;
+
+        for (int i = 0; i < strings.size(); i++) {
+            String line = strings.get(i);
+            if (line.length() > maxLineWidth) {
+                maxLineWidth = line.length();
+            }
+        }
+
+        setBottomValue(strings.size() - getHeight() + 1);
+        if (getBottomValue() < 0) {
+            setBottomValue(0);
+        }
+
+        setRightValue(maxLineWidth - getWidth() + 1);
+        if (getRightValue() < 0) {
+            setRightValue(0);
+        }
+    }
+
+    /**
+     * Draw the files list.
+     */
+    @Override
+    public void draw() {
+        CellAttributes color = null;
+        int begin = getVerticalValue();
+        int topY = 0;
+        for (int i = begin; i < strings.size(); i++) {
+            String line = strings.get(i);
+            if (getHorizontalValue() < line.length()) {
+                line = line.substring(getHorizontalValue());
+            } else {
+                line = "";
+            }
+            if (i == selectedString) {
+                if (isAbsoluteActive()) {
+                    color = getTheme().getColor("tlist.selected");
+                } else {
+                    color = getTheme().getColor("tlist.selected.inactive");
+                }
+            } else if (isAbsoluteActive()) {
+                color = getTheme().getColor("tlist");
+            } else {
+                color = getTheme().getColor("tlist.inactive");
+            }
+            String formatString = "%-" + Integer.toString(getWidth() - 1) + "s";
+            getScreen().putStringXY(0, topY, String.format(formatString, line),
+                    color);
+            topY++;
+            if (topY >= getHeight() - 1) {
+                break;
+            }
+        }
+
+        if (isAbsoluteActive()) {
+            color = getTheme().getColor("tlist");
+        } else {
+            color = getTheme().getColor("tlist.inactive");
+        }
+
+        // Pad the rest with blank lines
+        for (int i = topY; i < getHeight() - 1; i++) {
+            getScreen().hLineXY(0, i, getWidth() - 1, ' ', color);
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // TList ------------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the selection index.
+     *
+     * @return -1 if nothing is selected, otherwise the index into the list
+     */
+    public final int getSelectedIndex() {
+        return selectedString;
+    }
+
+    /**
+     * Set the selected string index.
+     *
+     * @param index -1 to unselect, otherwise the index into the list
+     */
+    public final void setSelectedIndex(final int index) {
+        selectedString = index;
+    }
+
+    /**
+     * Get the selected string.
+     *
+     * @return the selected string, or null of nothing is selected yet
+     */
+    public final String getSelected() {
+        if ((selectedString >= 0) && (selectedString <= strings.size() - 1)) {
+            return strings.get(selectedString);
+        }
+        return null;
+    }
+
+    /**
+     * Get the maximum selection index value.
+     *
+     * @return -1 if the list is empty
+     */
+    public final int getMaxSelectedIndex() {
+        return strings.size() - 1;
+    }
+
+    /**
+     * Set the new list of strings to display.
+     *
+     * @param list new list of strings
+     */
+    public final void setList(final List<String> list) {
+        strings.clear();
+        strings.addAll(list);
+        reflowData();
+    }
+
+    /**
+     * Perform user selection action.
+     */
+    public void dispatchEnter() {
+        assert (selectedString >= 0);
+        assert (selectedString < strings.size());
+        if (enterAction != null) {
+            enterAction.DO();
+        }
+    }
+
+    /**
+     * Perform list movement action.
+     */
+    public void dispatchMove() {
+        assert (selectedString >= 0);
+        assert (selectedString < strings.size());
+        if (moveAction != null) {
+            moveAction.DO();
+        }
+    }
+
 }
index 49fd76ae8cc899c79b53f4abffe90fb5d5026f51..0677199ad497aa3286f242810275791a761c74b5 100644 (file)
@@ -36,74 +36,28 @@ import jexer.bits.GraphicsChars;
  */
 public final class TProgressBar extends TWidget {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Value that corresponds to 0% progress.
      */
     private int minValue = 0;
 
-    /**
-     * Get the value that corresponds to 0% progress.
-     *
-     * @return the value that corresponds to 0% progress
-     */
-    public int getMinValue() {
-        return minValue;
-    }
-
-    /**
-     * Set the value that corresponds to 0% progress.
-     *
-     * @param minValue the value that corresponds to 0% progress
-     */
-    public void setMinValue(final int minValue) {
-        this.minValue = minValue;
-    }
-
     /**
      * Value that corresponds to 100% progress.
      */
     private int maxValue = 100;
 
-    /**
-     * Get the value that corresponds to 100% progress.
-     *
-     * @return the value that corresponds to 100% progress
-     */
-    public int getMaxValue() {
-        return maxValue;
-    }
-
-    /**
-     * Set the value that corresponds to 100% progress.
-     *
-     * @param maxValue the value that corresponds to 100% progress
-     */
-    public void setMaxValue(final int maxValue) {
-        this.maxValue = maxValue;
-    }
-
     /**
      * Current value of the progress.
      */
     private int value = 0;
 
-    /**
-     * Get the current value of the progress.
-     *
-     * @return the current value of the progress
-     */
-    public int getValue() {
-        return value;
-    }
-
-    /**
-     * Set the current value of the progress.
-     *
-     * @param value the current value of the progress
-     */
-    public void setValue(final int value) {
-        this.value = value;
-    }
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Public constructor.
@@ -123,6 +77,15 @@ public final class TProgressBar extends TWidget {
         this.value = value;
     }
 
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Draw a static progress bar.
      */
@@ -158,4 +121,62 @@ public final class TProgressBar extends TWidget {
             incompleteColor);
     }
 
+    // ------------------------------------------------------------------------
+    // TProgressBar -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the value that corresponds to 0% progress.
+     *
+     * @return the value that corresponds to 0% progress
+     */
+    public int getMinValue() {
+        return minValue;
+    }
+
+    /**
+     * Set the value that corresponds to 0% progress.
+     *
+     * @param minValue the value that corresponds to 0% progress
+     */
+    public void setMinValue(final int minValue) {
+        this.minValue = minValue;
+    }
+
+    /**
+     * Get the value that corresponds to 100% progress.
+     *
+     * @return the value that corresponds to 100% progress
+     */
+    public int getMaxValue() {
+        return maxValue;
+    }
+
+    /**
+     * Set the value that corresponds to 100% progress.
+     *
+     * @param maxValue the value that corresponds to 100% progress
+     */
+    public void setMaxValue(final int maxValue) {
+        this.maxValue = maxValue;
+    }
+
+    /**
+     * Get the current value of the progress.
+     *
+     * @return the current value of the progress
+     */
+    public int getValue() {
+        return value;
+    }
+
+    /**
+     * Set the current value of the progress.
+     *
+     * @param value the current value of the progress
+     */
+    public void setValue(final int value) {
+        this.value = value;
+    }
+
 }
index 13a73ffedd73dddd1f3e2fc5bc91d45f4a8bc7f7..cdb56aa35860d3734235ce0ff35a3e942b81352b 100644 (file)
@@ -39,32 +39,15 @@ import static jexer.TKeypress.*;
  */
 public final class TRadioButton extends TWidget {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * RadioButton state, true means selected.
      */
     private boolean selected = false;
 
-    /**
-     * Get RadioButton state, true means selected.
-     *
-     * @return if true then this is the one button in the group that is
-     * selected
-     */
-    public boolean isSelected() {
-        return selected;
-    }
-
-    /**
-     * Set RadioButton state, true means selected.  Note package private
-     * access.
-     *
-     * @param selected if true then this is the one button in the group that
-     * is selected
-     */
-    void setSelected(final boolean selected) {
-        this.selected = selected;
-    }
-
     /**
      * Label for this radio button.
      */
@@ -76,15 +59,9 @@ public final class TRadioButton extends TWidget {
      */
     private int id;
 
-    /**
-     * Get ID for this radio button.  Buttons start counting at 1 in the
-     * RadioGroup.
-     *
-     * @return the ID
-     */
-    public int getId() {
-        return id;
-    }
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Public constructor.
@@ -108,6 +85,10 @@ public final class TRadioButton extends TWidget {
         setCursorX(1);
     }
 
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Returns true if the mouse is currently on the radio button.
      *
@@ -124,30 +105,6 @@ public final class TRadioButton extends TWidget {
         return false;
     }
 
-    /**
-     * Draw a radio button with label.
-     */
-    @Override
-    public void draw() {
-        CellAttributes radioButtonColor;
-
-        if (isAbsoluteActive()) {
-            radioButtonColor = getTheme().getColor("tradiobutton.active");
-        } else {
-            radioButtonColor = getTheme().getColor("tradiobutton.inactive");
-        }
-
-        getScreen().putCharXY(0, 0, '(', radioButtonColor);
-        if (selected) {
-            getScreen().putCharXY(1, 0, GraphicsChars.CP437[0x07],
-                radioButtonColor);
-        } else {
-            getScreen().putCharXY(1, 0, ' ', radioButtonColor);
-        }
-        getScreen().putCharXY(2, 0, ')', radioButtonColor);
-        getScreen().putStringXY(4, 0, label, radioButtonColor);
-    }
-
     /**
      * Handle mouse button presses.
      *
@@ -184,4 +141,67 @@ public final class TRadioButton extends TWidget {
         super.onKeypress(keypress);
     }
 
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw a radio button with label.
+     */
+    @Override
+    public void draw() {
+        CellAttributes radioButtonColor;
+
+        if (isAbsoluteActive()) {
+            radioButtonColor = getTheme().getColor("tradiobutton.active");
+        } else {
+            radioButtonColor = getTheme().getColor("tradiobutton.inactive");
+        }
+
+        getScreen().putCharXY(0, 0, '(', radioButtonColor);
+        if (selected) {
+            getScreen().putCharXY(1, 0, GraphicsChars.CP437[0x07],
+                radioButtonColor);
+        } else {
+            getScreen().putCharXY(1, 0, ' ', radioButtonColor);
+        }
+        getScreen().putCharXY(2, 0, ')', radioButtonColor);
+        getScreen().putStringXY(4, 0, label, radioButtonColor);
+    }
+
+    // ------------------------------------------------------------------------
+    // TRadioButton -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get RadioButton state, true means selected.
+     *
+     * @return if true then this is the one button in the group that is
+     * selected
+     */
+    public boolean isSelected() {
+        return selected;
+    }
+
+    /**
+     * Set RadioButton state, true means selected.  Note package private
+     * access.
+     *
+     * @param selected if true then this is the one button in the group that
+     * is selected
+     */
+    void setSelected(final boolean selected) {
+        this.selected = selected;
+    }
+
+    /**
+     * Get ID for this radio button.  Buttons start counting at 1 in the
+     * RadioGroup.
+     *
+     * @return the ID
+     */
+    public int getId() {
+        return id;
+    }
+
 }
index 60b100a5154530e740fa0a9307ce2299ea9d57d6..975d285e4fa33082cf7415c7b345c9456a19729d 100644 (file)
@@ -29,6 +29,7 @@
 package jexer;
 
 import java.util.ArrayList;
+import java.util.List;
 
 import jexer.bits.CellAttributes;
 import jexer.bits.GraphicsChars;
@@ -41,6 +42,25 @@ import jexer.event.TMouseEvent;
  */
 public final class TStatusBar extends TWidget {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Remember mouse state.
+     */
+    private TMouseEvent mouse;
+
+    /**
+     * The text to display on the right side of the shortcut keys.
+     */
+    private String text = null;
+
+    /**
+     * The shortcut keys.
+     */
+    private List<TStatusBarKey> keys = new ArrayList<TStatusBarKey>();
+
     /**
      * A single shortcut key.
      */
@@ -98,48 +118,9 @@ public final class TStatusBar extends TWidget {
 
     }
 
-    /**
-     * Remember mouse state.
-     */
-    private TMouseEvent mouse;
-
-    /**
-     * The text to display on the right side of the shortcut keys.
-     */
-    private String text = null;
-
-    /**
-     * The shortcut keys.
-     */
-    private ArrayList<TStatusBarKey> keys = new ArrayList<TStatusBarKey>();
-
-    /**
-     * Add a key to this status bar.
-     *
-     * @param key the key to trigger on
-     * @param cmd the command event to issue when key is pressed or this item
-     * is clicked
-     * @param label the label for this action
-     */
-    public void addShortcutKeypress(final TKeypress key, final TCommand cmd,
-        final String label) {
-
-        TStatusBarKey newKey = new TStatusBarKey(key, cmd, label);
-        if (keys.size() > 0) {
-            TStatusBarKey oldKey = keys.get(keys.size() - 1);
-            newKey.x = oldKey.x + oldKey.width();
-        }
-        keys.add(newKey);
-    }
-
-    /**
-     * Set the text to display on the right side of the shortcut keys.
-     *
-     * @param text the new text
-     */
-    public void setText(final String text) {
-        this.text = text;
-    }
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Public constructor.
@@ -164,56 +145,9 @@ public final class TStatusBar extends TWidget {
         this(parent, "");
     }
 
-    /**
-     * Draw the bar.
-     */
-    @Override
-    public void draw() {
-        CellAttributes barColor = new CellAttributes();
-        barColor.setTo(getTheme().getColor("tstatusbar.text"));
-        CellAttributes keyColor = new CellAttributes();
-        keyColor.setTo(getTheme().getColor("tstatusbar.button"));
-        CellAttributes selectedColor = new CellAttributes();
-        selectedColor.setTo(getTheme().getColor("tstatusbar.selected"));
-
-        // Status bar is weird.  Its draw() method is called directly by
-        // TApplication after everything is drawn, and after
-        // Screen.resetClipping().  So at this point we are drawing in
-        // absolute coordinates, not relative to our TWindow.
-        int row = getScreen().getHeight() - 1;
-        int width = getScreen().getWidth();
-
-        getScreen().hLineXY(0, row, width, ' ', barColor);
-
-        int col = 0;
-        for (TStatusBarKey key: keys) {
-            String keyStr = key.key.toString();
-            if (key.selected) {
-                getScreen().putCharXY(col++, row, ' ', selectedColor);
-                getScreen().putStringXY(col, row, keyStr, selectedColor);
-                col += keyStr.length();
-                getScreen().putCharXY(col++, row, ' ', selectedColor);
-                getScreen().putStringXY(col, row, key.label, selectedColor);
-                col += key.label.length();
-                getScreen().putCharXY(col++, row, ' ', selectedColor);
-            } else {
-                getScreen().putCharXY(col++, row, ' ', barColor);
-                getScreen().putStringXY(col, row, keyStr, keyColor);
-                col += keyStr.length() + 1;
-                getScreen().putStringXY(col, row, key.label, barColor);
-                col += key.label.length();
-                getScreen().putCharXY(col++, row, ' ', barColor);
-            }
-        }
-        if (text.length() > 0) {
-            if (keys.size() > 0) {
-                getScreen().putCharXY(col++, row, GraphicsChars.VERTICAL_BAR,
-                    barColor);
-            }
-            getScreen().putCharXY(col++, row, ' ', barColor);
-            getScreen().putStringXY(col, row, text, barColor);
-        }
-    }
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Handle keypresses.
@@ -302,4 +236,91 @@ public final class TStatusBar extends TWidget {
         }
     }
 
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw the bar.
+     */
+    @Override
+    public void draw() {
+        CellAttributes barColor = new CellAttributes();
+        barColor.setTo(getTheme().getColor("tstatusbar.text"));
+        CellAttributes keyColor = new CellAttributes();
+        keyColor.setTo(getTheme().getColor("tstatusbar.button"));
+        CellAttributes selectedColor = new CellAttributes();
+        selectedColor.setTo(getTheme().getColor("tstatusbar.selected"));
+
+        // Status bar is weird.  Its draw() method is called directly by
+        // TApplication after everything is drawn, and after
+        // Screen.resetClipping().  So at this point we are drawing in
+        // absolute coordinates, not relative to our TWindow.
+        int row = getScreen().getHeight() - 1;
+        int width = getScreen().getWidth();
+
+        getScreen().hLineXY(0, row, width, ' ', barColor);
+
+        int col = 0;
+        for (TStatusBarKey key: keys) {
+            String keyStr = key.key.toString();
+            if (key.selected) {
+                getScreen().putCharXY(col++, row, ' ', selectedColor);
+                getScreen().putStringXY(col, row, keyStr, selectedColor);
+                col += keyStr.length();
+                getScreen().putCharXY(col++, row, ' ', selectedColor);
+                getScreen().putStringXY(col, row, key.label, selectedColor);
+                col += key.label.length();
+                getScreen().putCharXY(col++, row, ' ', selectedColor);
+            } else {
+                getScreen().putCharXY(col++, row, ' ', barColor);
+                getScreen().putStringXY(col, row, keyStr, keyColor);
+                col += keyStr.length() + 1;
+                getScreen().putStringXY(col, row, key.label, barColor);
+                col += key.label.length();
+                getScreen().putCharXY(col++, row, ' ', barColor);
+            }
+        }
+        if (text.length() > 0) {
+            if (keys.size() > 0) {
+                getScreen().putCharXY(col++, row, GraphicsChars.VERTICAL_BAR,
+                    barColor);
+            }
+            getScreen().putCharXY(col++, row, ' ', barColor);
+            getScreen().putStringXY(col, row, text, barColor);
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // TStatusBar -------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Add a key to this status bar.
+     *
+     * @param key the key to trigger on
+     * @param cmd the command event to issue when key is pressed or this item
+     * is clicked
+     * @param label the label for this action
+     */
+    public void addShortcutKeypress(final TKeypress key, final TCommand cmd,
+        final String label) {
+
+        TStatusBarKey newKey = new TStatusBarKey(key, cmd, label);
+        if (keys.size() > 0) {
+            TStatusBarKey oldKey = keys.get(keys.size() - 1);
+            newKey.x = oldKey.x + oldKey.width();
+        }
+        keys.add(newKey);
+    }
+
+    /**
+     * Set the text to display on the right side of the shortcut keys.
+     *
+     * @param text the new text
+     */
+    public void setText(final String text) {
+        this.text = text;
+    }
+
 }
index c57e8846ace8b8ee160d250147333bd63723ab76..d0148f202a047b4b60f15dff444abd34e6150e28 100644 (file)
  */
 package jexer;
 
-import static jexer.TKeypress.kbDown;
-import static jexer.TKeypress.kbEnd;
-import static jexer.TKeypress.kbHome;
-import static jexer.TKeypress.kbLeft;
-import static jexer.TKeypress.kbPgDn;
-import static jexer.TKeypress.kbPgUp;
-import static jexer.TKeypress.kbRight;
-import static jexer.TKeypress.kbUp;
-
 import java.util.LinkedList;
 import java.util.List;
 
 import jexer.bits.CellAttributes;
 import jexer.event.TKeypressEvent;
 import jexer.event.TMouseEvent;
+import static jexer.TKeypress.*;
 
 /**
  * TText implements a simple scrollable text area. It reflows automatically on
@@ -224,19 +216,19 @@ public final class TText extends TScrollableWidget {
         for (String p : paragraphs) {
             switch (justification) {
             case LEFT:
-                lines.addAll(jexer.bits.StringJustifier.left(p,
+                lines.addAll(jexer.bits.StringUtils.left(p,
                         getWidth() - 1));
                 break;
             case CENTER:
-                lines.addAll(jexer.bits.StringJustifier.center(p,
+                lines.addAll(jexer.bits.StringUtils.center(p,
                         getWidth() - 1));
                 break;
             case RIGHT:
-                lines.addAll(jexer.bits.StringJustifier.right(p,
+                lines.addAll(jexer.bits.StringUtils.right(p,
                         getWidth() - 1));
                 break;
             case FULL:
-                lines.addAll(jexer.bits.StringJustifier.full(p,
+                lines.addAll(jexer.bits.StringUtils.full(p,
                         getWidth() - 1));
                 break;
             }
index a86c132febbcfb31977ef6a73989e3846905d2ce..5d3dc134f9002bfb3c1e48a8eee1f1d0f02f9a2b 100644 (file)
@@ -35,6 +35,10 @@ import java.util.Date;
  */
 public final class TTimer {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * If true, re-schedule after every tick.  Note package private access.
      */
@@ -50,6 +54,36 @@ public final class TTimer {
      */
     private Date nextTick;
 
+    /**
+     * The action to perfom on a tick.
+     */
+    private TAction action;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Package private constructor.
+     *
+     * @param duration number of milliseconds to wait between ticks
+     * @param recurring if true, re-schedule this timer after every tick
+     * @param action to perform on next tick
+     */
+    TTimer(final long duration, final boolean recurring, final TAction action) {
+
+        this.recurring = recurring;
+        this.duration  = duration;
+        this.action    = action;
+
+        Date now = new Date();
+        nextTick = new Date(now.getTime() + duration);
+    }
+
+    // ------------------------------------------------------------------------
+    // TTimer -----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Get the next time this timer needs to be ticked.  Note package private
      * access.
@@ -69,11 +103,6 @@ public final class TTimer {
         this.recurring = recurring;
     }
 
-    /**
-     * The action to perfom on a tick.
-     */
-    private TAction action;
-
     /**
      * Tick this timer.  Note package private access.
      */
@@ -88,21 +117,4 @@ public final class TTimer {
         }
     }
 
-    /**
-     * Package private constructor.
-     *
-     * @param duration number of milliseconds to wait between ticks
-     * @param recurring if true, re-schedule this timer after every tick
-     * @param action to perform on next tick
-     */
-    TTimer(final long duration, final boolean recurring, final TAction action) {
-
-        this.recurring = recurring;
-        this.duration  = duration;
-        this.action    = action;
-
-        Date now = new Date();
-        nextTick = new Date(now.getTime() + duration);
-    }
-
 }
index 18178807fc10d14e86cfcdfc9c3e385de102c0cf..7a88739d69fb6e9bafcaf4da6f6fce64f20765b7 100644 (file)
@@ -37,11 +37,202 @@ import jexer.event.TMouseEvent;
  */
 public final class TVScroller extends TWidget {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Value that corresponds to being on the top edge of the scroll bar.
      */
     private int topValue = 0;
 
+    /**
+     * Value that corresponds to being on the bottom edge of the scroll bar.
+     */
+    private int bottomValue = 100;
+
+    /**
+     * Current value of the scroll.
+     */
+    private int value = 0;
+
+    /**
+     * The increment for clicking on an arrow.
+     */
+    private int smallChange = 1;
+
+    /**
+     * The increment for clicking in the bar between the box and an arrow.
+     */
+    private int bigChange = 20;
+
+    /**
+     * When true, the user is dragging the scroll box.
+     */
+    private boolean inScroll = false;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param height height of scroll bar
+     */
+    public TVScroller(final TWidget parent, final int x, final int y,
+        final int height) {
+
+        // Set parent and window
+        super(parent, x, y, 1, height);
+    }
+
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Handle mouse button releases.
+     *
+     * @param mouse mouse button release event
+     */
+    @Override
+    public void onMouseUp(final TMouseEvent mouse) {
+        if (bottomValue == topValue) {
+            return;
+        }
+
+        if (inScroll) {
+            inScroll = false;
+            return;
+        }
+
+        if ((mouse.getX() == 0)
+            && (mouse.getY() == 0)
+        ) {
+            // Clicked on the top arrow
+            decrement();
+            return;
+        }
+
+        if ((mouse.getX() == 0)
+            && (mouse.getY() == getHeight() - 1)
+        ) {
+            // Clicked on the bottom arrow
+            increment();
+            return;
+        }
+
+        if ((mouse.getX() == 0)
+            && (mouse.getY() > 0)
+            && (mouse.getY() < boxPosition())
+        ) {
+            // Clicked between the top arrow and the box
+            value -= bigChange;
+            if (value < topValue) {
+                value = topValue;
+            }
+            return;
+        }
+
+        if ((mouse.getX() == 0)
+            && (mouse.getY() > boxPosition())
+            && (mouse.getY() < getHeight() - 1)
+        ) {
+            // Clicked between the box and the bottom arrow
+            value += bigChange;
+            if (value > bottomValue) {
+                value = bottomValue;
+            }
+            return;
+        }
+    }
+
+    /**
+     * Handle mouse movement events.
+     *
+     * @param mouse mouse motion event
+     */
+    @Override
+    public void onMouseMotion(final TMouseEvent mouse) {
+        if (bottomValue == topValue) {
+            return;
+        }
+
+        if ((mouse.isMouse1())
+            && (inScroll)
+            && (mouse.getY() > 0)
+            && (mouse.getY() < getHeight() - 1)
+        ) {
+            // Recompute value based on new box position
+            value = (bottomValue - topValue)
+                * (mouse.getY()) / (getHeight() - 3) + topValue;
+            if (value > bottomValue) {
+                value = bottomValue;
+            }
+            if (value < topValue) {
+                value = topValue;
+            }
+            return;
+        }
+
+        inScroll = false;
+    }
+
+    /**
+     * Handle mouse press events.
+     *
+     * @param mouse mouse button press event
+     */
+    @Override
+    public void onMouseDown(final TMouseEvent mouse) {
+        if (bottomValue == topValue) {
+            return;
+        }
+
+        if ((mouse.getX() == 0)
+            && (mouse.getY() == boxPosition())
+        ) {
+            inScroll = true;
+            return;
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw a vertical scroll bar.
+     */
+    @Override
+    public void draw() {
+        CellAttributes arrowColor = getTheme().getColor("tscroller.arrows");
+        CellAttributes barColor = getTheme().getColor("tscroller.bar");
+        getScreen().putCharXY(0, 0, GraphicsChars.CP437[0x1E], arrowColor);
+        getScreen().putCharXY(0, getHeight() - 1, GraphicsChars.CP437[0x1F],
+            arrowColor);
+
+        // Place the box
+        if (bottomValue > topValue) {
+            getScreen().vLineXY(0, 1, getHeight() - 2,
+                GraphicsChars.CP437[0xB1], barColor);
+            getScreen().putCharXY(0, boxPosition(), GraphicsChars.BOX,
+                arrowColor);
+        } else {
+            getScreen().vLineXY(0, 1, getHeight() - 2, GraphicsChars.HATCH,
+                barColor);
+        }
+
+    }
+
+    // ------------------------------------------------------------------------
+    // TVScroller -------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Get the value that corresponds to being on the top edge of the scroll
      * bar.
@@ -62,11 +253,6 @@ public final class TVScroller extends TWidget {
         this.topValue = topValue;
     }
 
-    /**
-     * Value that corresponds to being on the bottom edge of the scroll bar.
-     */
-    private int bottomValue = 100;
-
     /**
      * Get the value that corresponds to being on the bottom edge of the
      * scroll bar.
@@ -87,11 +273,6 @@ public final class TVScroller extends TWidget {
         this.bottomValue = bottomValue;
     }
 
-    /**
-     * Current value of the scroll.
-     */
-    private int value = 0;
-
     /**
      * Get current value of the scroll.
      *
@@ -110,11 +291,6 @@ public final class TVScroller extends TWidget {
         this.value = value;
     }
 
-    /**
-     * The increment for clicking on an arrow.
-     */
-    private int smallChange = 1;
-
     /**
      * Get the increment for clicking on an arrow.
      *
@@ -133,11 +309,6 @@ public final class TVScroller extends TWidget {
         this.smallChange = smallChange;
     }
 
-    /**
-     * The increment for clicking in the bar between the box and an arrow.
-     */
-    private int bigChange = 20;
-
     /**
      * Set the increment for clicking in the bar between the box and an
      * arrow.
@@ -158,26 +329,6 @@ public final class TVScroller extends TWidget {
         this.bigChange = bigChange;
     }
 
-    /**
-     * When true, the user is dragging the scroll box.
-     */
-    private boolean inScroll = false;
-
-    /**
-     * Public constructor.
-     *
-     * @param parent parent widget
-     * @param x column relative to parent
-     * @param y row relative to parent
-     * @param height height of scroll bar
-     */
-    public TVScroller(final TWidget parent, final int x, final int y,
-        final int height) {
-
-        // Set parent and window
-        super(parent, x, y, 1, height);
-    }
-
     /**
      * Compute the position of the scroll box (a.k.a. grip, thumb).
      *
@@ -187,30 +338,6 @@ public final class TVScroller extends TWidget {
         return (getHeight() - 3) * (value - topValue) / (bottomValue - topValue) + 1;
     }
 
-    /**
-     * Draw a vertical scroll bar.
-     */
-    @Override
-    public void draw() {
-        CellAttributes arrowColor = getTheme().getColor("tscroller.arrows");
-        CellAttributes barColor = getTheme().getColor("tscroller.bar");
-        getScreen().putCharXY(0, 0, GraphicsChars.CP437[0x1E], arrowColor);
-        getScreen().putCharXY(0, getHeight() - 1, GraphicsChars.CP437[0x1F],
-            arrowColor);
-
-        // Place the box
-        if (bottomValue > topValue) {
-            getScreen().vLineXY(0, 1, getHeight() - 2,
-                GraphicsChars.CP437[0xB1], barColor);
-            getScreen().putCharXY(0, boxPosition(), GraphicsChars.BOX,
-                arrowColor);
-        } else {
-            getScreen().vLineXY(0, 1, getHeight() - 2, GraphicsChars.HATCH,
-                barColor);
-        }
-
-    }
-
     /**
      * Perform a small step change up.
      */
@@ -277,105 +404,4 @@ public final class TVScroller extends TWidget {
         value = bottomValue;
     }
 
-    /**
-     * Handle mouse button releases.
-     *
-     * @param mouse mouse button release event
-     */
-    @Override
-    public void onMouseUp(final TMouseEvent mouse) {
-        if (bottomValue == topValue) {
-            return;
-        }
-
-        if (inScroll) {
-            inScroll = false;
-            return;
-        }
-
-        if ((mouse.getX() == 0)
-            && (mouse.getY() == 0)
-        ) {
-            // Clicked on the top arrow
-            decrement();
-            return;
-        }
-
-        if ((mouse.getX() == 0)
-            && (mouse.getY() == getHeight() - 1)
-        ) {
-            // Clicked on the bottom arrow
-            increment();
-            return;
-        }
-
-        if ((mouse.getX() == 0)
-            && (mouse.getY() > 0)
-            && (mouse.getY() < boxPosition())
-        ) {
-            // Clicked between the top arrow and the box
-            value -= bigChange;
-            if (value < topValue) {
-                value = topValue;
-            }
-            return;
-        }
-
-        if ((mouse.getX() == 0)
-            && (mouse.getY() > boxPosition())
-            && (mouse.getY() < getHeight() - 1)
-        ) {
-            // Clicked between the box and the bottom arrow
-            value += bigChange;
-            if (value > bottomValue) {
-                value = bottomValue;
-            }
-            return;
-        }
-    }
-
-    /**
-     * Handle mouse movement events.
-     *
-     * @param mouse mouse motion event
-     */
-    @Override
-    public void onMouseMotion(final TMouseEvent mouse) {
-        if (bottomValue == topValue) {
-            return;
-        }
-
-        if ((mouse.isMouse1())
-            && (inScroll)
-            && (mouse.getY() > 0)
-            && (mouse.getY() < getHeight() - 1)
-        ) {
-            // Recompute value based on new box position
-            value = (bottomValue - topValue)
-                * (mouse.getY()) / (getHeight() - 3) + topValue;
-            return;
-        }
-
-        inScroll = false;
-    }
-
-    /**
-     * Handle mouse press events.
-     *
-     * @param mouse mouse button press event
-     */
-    @Override
-    public void onMouseDown(final TMouseEvent mouse) {
-        if (bottomValue == topValue) {
-            return;
-        }
-
-        if ((mouse.getX() == 0)
-            && (mouse.getY() == boxPosition())
-        ) {
-            inScroll = true;
-            return;
-        }
-    }
-
 }
index 169e5576be85d09a4aedd865ecc680a84d45e2f9..7d2ea3572b3c7f582b052602528326e5cbc0eabe 100644 (file)
@@ -41,6 +41,9 @@ import jexer.event.TMenuEvent;
 import jexer.event.TMouseEvent;
 import jexer.event.TResizeEvent;
 import jexer.menu.TMenu;
+import jexer.ttree.TTreeItem;
+import jexer.ttree.TTreeView;
+import jexer.ttree.TTreeViewWidget;
 import static jexer.TKeypress.*;
 
 /**
@@ -50,7 +53,7 @@ import static jexer.TKeypress.*;
 public abstract class TWidget implements Comparable<TWidget> {
 
     // ------------------------------------------------------------------------
-    // Common widget attributes -----------------------------------------------
+    // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
 
     /**
@@ -60,29 +63,11 @@ public abstract class TWidget implements Comparable<TWidget> {
      */
     private TWidget parent = null;
 
-    /**
-     * Get parent widget.
-     *
-     * @return parent widget
-     */
-    public final TWidget getParent() {
-        return parent;
-    }
-
     /**
      * Child widgets that this widget contains.
      */
     private List<TWidget> children;
 
-    /**
-     * Get the list of child widgets that this widget contains.
-     *
-     * @return the list of child widgets
-     */
-    public List<TWidget> getChildren() {
-        return children;
-    }
-
     /**
      * The currently active child widget that will receive keypress events.
      */
@@ -93,1024 +78,1058 @@ public abstract class TWidget implements Comparable<TWidget> {
      */
     private boolean active = false;
 
-    /**
-     * Get active flag.
-     *
-     * @return if true, this widget will receive events
-     */
-    public final boolean isActive() {
-        return active;
-    }
-
-    /**
-     * Set active flag.
-     *
-     * @param active if true, this widget will receive events
-     */
-    public final void setActive(final boolean active) {
-        this.active = active;
-    }
-
     /**
      * The window that this widget draws to.
      */
     private TWindow window = null;
 
-    /**
-     * Get the window this widget is on.
-     *
-     * @return the window
-     */
-    public final TWindow getWindow() {
-        return window;
-    }
-
     /**
      * Absolute X position of the top-left corner.
      */
     private int x = 0;
 
     /**
-     * Get X position.
-     *
-     * @return absolute X position of the top-left corner
+     * Absolute Y position of the top-left corner.
      */
-    public final int getX() {
-        return x;
-    }
+    private int y = 0;
 
     /**
-     * Set X position.
-     *
-     * @param x absolute X position of the top-left corner
+     * Width.
      */
-    public final void setX(final int x) {
-        this.x = x;
-    }
+    private int width = 0;
 
     /**
-     * Absolute Y position of the top-left corner.
+     * Height.
      */
-    private int y = 0;
+    private int height = 0;
 
     /**
-     * Get Y position.
-     *
-     * @return absolute Y position of the top-left corner
+     * My tab order inside a window or containing widget.
      */
-    public final int getY() {
-        return y;
-    }
+    private int tabOrder = 0;
 
     /**
-     * Set Y position.
-     *
-     * @param y absolute Y position of the top-left corner
+     * If true, this widget can be tabbed to or receive events.
      */
-    public final void setY(final int y) {
-        this.y = y;
-    }
+    private boolean enabled = true;
 
     /**
-     * Width.
+     * If true, this widget has a cursor.
      */
-    private int width = 0;
+    private boolean cursorVisible = false;
 
     /**
-     * Get the width.
-     *
-     * @return widget width
+     * Cursor column position in relative coordinates.
      */
-    public final int getWidth() {
-        return this.width;
-    }
+    private int cursorX = 0;
 
     /**
-     * Change the width.
-     *
-     * @param width new widget width
+     * Cursor row position in relative coordinates.
      */
-    public final void setWidth(final int width) {
-        this.width = width;
-    }
+    private int cursorY = 0;
 
-    /**
-     * Height.
-     */
-    private int height = 0;
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
-     * Get the height.
-     *
-     * @return widget height
+     * Default constructor for subclasses.
      */
-    public final int getHeight() {
-        return this.height;
+    protected TWidget() {
+        children = new ArrayList<TWidget>();
     }
 
     /**
-     * Change the height.
+     * Protected constructor.
      *
-     * @param height new widget height
+     * @param parent parent widget
      */
-    public final void setHeight(final int height) {
-        this.height = height;
+    protected TWidget(final TWidget parent) {
+        this(parent, true);
     }
 
     /**
-     * Change the dimensions.
+     * Protected constructor.
      *
-     * @param x absolute X position of the top-left corner
-     * @param y absolute Y position of the top-left corner
-     * @param width new widget width
-     * @param height new widget height
+     * @param parent parent widget
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of widget
+     * @param height height of widget
      */
-    public final void setDimensions(final int x, final int y, final int width,
-        final int height) {
+    protected TWidget(final TWidget parent, final int x, final int y,
+        final int width, final int height) {
 
-        setX(x);
-        setY(y);
-        setWidth(width);
-        setHeight(height);
+        this(parent, true, x, y, width, height);
     }
 
     /**
-     * My tab order inside a window or containing widget.
-     */
-    private int tabOrder = 0;
-
-    /**
-     * If true, this widget can be tabbed to or receive events.
-     */
-    private boolean enabled = true;
-
-    /**
-     * Get enabled flag.
+     * Protected constructor used by subclasses that are disabled by default.
      *
-     * @return if true, this widget can be tabbed to or receive events
+     * @param parent parent widget
+     * @param enabled if true assume enabled
      */
-    public final boolean isEnabled() {
-        return enabled;
+    protected TWidget(final TWidget parent, final boolean enabled) {
+        this.enabled = enabled;
+        this.parent = parent;
+        this.window = parent.window;
+        children = new ArrayList<TWidget>();
+
+        // Do not add TStatusBars, they are drawn by TApplication
+        if (this instanceof TStatusBar) {
+        } else {
+            parent.addChild(this);
+        }
     }
 
     /**
-     * Set enabled flag.
+     * Protected constructor used by subclasses that are disabled by default.
      *
-     * @param enabled if true, this widget can be tabbed to or receive events
+     * @param parent parent widget
+     * @param enabled if true assume enabled
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of widget
+     * @param height height of widget
      */
-    public final void setEnabled(final boolean enabled) {
+    protected TWidget(final TWidget parent, final boolean enabled,
+        final int x, final int y, final int width, final int height) {
+
         this.enabled = enabled;
-        if (!enabled) {
-            active = false;
-            // See if there are any active siblings to switch to
-            boolean foundSibling = false;
-            if (parent != null) {
-                for (TWidget w: parent.children) {
-                    if ((w.enabled)
-                        && !(this instanceof THScroller)
-                        && !(this instanceof TVScroller)
-                    ) {
-                        parent.activate(w);
-                        foundSibling = true;
-                        break;
-                    }
-                }
-                if (!foundSibling) {
-                    parent.activeChild = null;
-                }
-            }
+        this.parent = parent;
+        this.window = parent.window;
+        children = new ArrayList<TWidget>();
+
+        // Do not add TStatusBars, they are drawn by TApplication
+        if (this instanceof TStatusBar) {
+        } else {
+            parent.addChild(this);
         }
-    }
 
-    /**
-     * If true, this widget has a cursor.
-     */
-    private boolean cursorVisible = false;
+        this.x = x;
+        this.y = y;
+        this.width = width;
+        this.height = height;
+    }
 
     /**
-     * Set visible cursor flag.
+     * Backdoor access for TWindow's constructor.  ONLY TWindow USES THIS.
      *
-     * @param cursorVisible if true, this widget has a cursor
+     * @param window the top-level window
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of window
+     * @param height height of window
      */
-    public final void setCursorVisible(final boolean cursorVisible) {
-        this.cursorVisible = cursorVisible;
+    protected final void setupForTWindow(final TWindow window,
+        final int x, final int y, final int width, final int height) {
+
+        this.parent = window;
+        this.window = window;
+        this.x      = x;
+        this.y      = y;
+        this.width  = width;
+        this.height = height;
     }
 
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
-     * See if this widget has a visible cursor.
+     * Check if a mouse press/release event coordinate is contained in this
+     * widget.
      *
-     * @return if true, this widget has a visible cursor
+     * @param mouse a mouse-based event
+     * @return whether or not a mouse click would be sent to this widget
      */
-    public final boolean isCursorVisible() {
-        // If cursor is out of my bounds, it is not visible.
-        if ((cursorX >= width)
-            || (cursorX < 0)
-            || (cursorY >= height)
-            || (cursorY < 0)
-        ) {
+    public final boolean mouseWouldHit(final TMouseEvent mouse) {
+
+        if (!enabled) {
             return false;
         }
 
-        // If cursor is out of my window's bounds, it is not visible.
-        if ((getCursorAbsoluteX() >= window.getAbsoluteX()
-                + window.getWidth() - 1)
-            || (getCursorAbsoluteX() < 0)
-            || (getCursorAbsoluteY() >= window.getAbsoluteY()
-                + window.getHeight() - 1)
-            || (getCursorAbsoluteY() < 0)
+        if ((this instanceof TTreeItem)
+            && ((y < 0) || (y > parent.getHeight() - 1))
         ) {
             return false;
         }
-        return cursorVisible;
-    }
 
-    /**
-     * Cursor column position in relative coordinates.
-     */
-    private int cursorX = 0;
+        if ((mouse.getAbsoluteX() >= getAbsoluteX())
+            && (mouse.getAbsoluteX() <  getAbsoluteX() + width)
+            && (mouse.getAbsoluteY() >= getAbsoluteY())
+            && (mouse.getAbsoluteY() <  getAbsoluteY() + height)
+        ) {
+            return true;
+        }
+        return false;
+    }
 
     /**
-     * Get cursor X value.
+     * Method that subclasses can override to handle keystrokes.
      *
-     * @return cursor column position in relative coordinates
+     * @param keypress keystroke event
      */
-    public final int getCursorX() {
-        return cursorX;
+    public void onKeypress(final TKeypressEvent keypress) {
+
+        if ((children.size() == 0)
+            || (this instanceof TTreeView)
+            || (this instanceof TText)
+        ) {
+
+            // Defaults:
+            //   tab / shift-tab - switch to next/previous widget
+            //   left-arrow or up-arrow: same as shift-tab
+            if ((keypress.equals(kbTab))
+                || (keypress.equals(kbDown))
+            ) {
+                parent.switchWidget(true);
+                return;
+            } else if ((keypress.equals(kbShiftTab))
+                || (keypress.equals(kbBackTab))
+                || (keypress.equals(kbUp))
+            ) {
+                parent.switchWidget(false);
+                return;
+            }
+        }
+
+        if ((children.size() == 0)
+            && !(this instanceof TTreeView)
+        ) {
+
+            // Defaults:
+            //   right-arrow or down-arrow: same as tab
+            if (keypress.equals(kbRight)) {
+                parent.switchWidget(true);
+                return;
+            } else if (keypress.equals(kbLeft)) {
+                parent.switchWidget(false);
+                return;
+            }
+        }
+
+        // If I have any buttons on me AND this is an Alt-key that matches
+        // its mnemonic, send it an Enter keystroke
+        for (TWidget widget: children) {
+            if (widget instanceof TButton) {
+                TButton button = (TButton) widget;
+                if (button.isEnabled()
+                    && !keypress.getKey().isFnKey()
+                    && keypress.getKey().isAlt()
+                    && !keypress.getKey().isCtrl()
+                    && (Character.toLowerCase(button.getMnemonic().getShortcut())
+                        == Character.toLowerCase(keypress.getKey().getChar()))
+                ) {
+
+                    widget.onKeypress(new TKeypressEvent(kbEnter));
+                    return;
+                }
+            }
+        }
+
+        // Dispatch the keypress to an active widget
+        for (TWidget widget: children) {
+            if (widget.active) {
+                widget.onKeypress(keypress);
+                return;
+            }
+        }
     }
 
     /**
-     * Set cursor X value.
+     * Method that subclasses can override to handle mouse button presses.
      *
-     * @param cursorX column position in relative coordinates
+     * @param mouse mouse button event
      */
-    public final void setCursorX(final int cursorX) {
-        this.cursorX = cursorX;
-    }
+    public void onMouseDown(final TMouseEvent mouse) {
+        // Default: do nothing, pass to children instead
+        for (int i = children.size() - 1 ; i >= 0 ; i--) {
+            TWidget widget = children.get(i);
+            if (widget.mouseWouldHit(mouse)) {
+                // Dispatch to this child, also activate it
+                activate(widget);
 
-    /**
-     * Cursor row position in relative coordinates.
-     */
-    private int cursorY = 0;
+                // Set x and y relative to the child's coordinates
+                mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+                mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+                widget.onMouseDown(mouse);
+                return;
+            }
+        }
+    }
 
     /**
-     * Get cursor Y value.
+     * Method that subclasses can override to handle mouse button releases.
      *
-     * @return cursor row position in relative coordinates
+     * @param mouse mouse button event
      */
-    public final int getCursorY() {
-        return cursorY;
+    public void onMouseUp(final TMouseEvent mouse) {
+        // Default: do nothing, pass to children instead
+        for (int i = children.size() - 1 ; i >= 0 ; i--) {
+            TWidget widget = children.get(i);
+            if (widget.mouseWouldHit(mouse)) {
+                // Dispatch to this child, also activate it
+                activate(widget);
+
+                // Set x and y relative to the child's coordinates
+                mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+                mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+                widget.onMouseUp(mouse);
+                return;
+            }
+        }
     }
 
     /**
-     * Set cursor Y value.
+     * Method that subclasses can override to handle mouse movements.
      *
-     * @param cursorY row position in relative coordinates
+     * @param mouse mouse motion event
      */
-    public final void setCursorY(final int cursorY) {
-        this.cursorY = cursorY;
+    public void onMouseMotion(final TMouseEvent mouse) {
+        // Default: do nothing, pass it on to ALL of my children.  This way
+        // the children can see the mouse "leaving" their area.
+        for (TWidget widget: children) {
+            // Set x and y relative to the child's coordinates
+            mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+            mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+            widget.onMouseMotion(mouse);
+        }
     }
 
-    // ------------------------------------------------------------------------
-    // TApplication integration -----------------------------------------------
-    // ------------------------------------------------------------------------
-
     /**
-     * Get this TWidget's parent TApplication.
+     * Method that subclasses can override to handle mouse button
+     * double-clicks.
      *
-     * @return the parent TApplication
+     * @param mouse mouse button event
      */
-    public TApplication getApplication() {
-        return window.getApplication();
+    public void onMouseDoubleClick(final TMouseEvent mouse) {
+        // Default: do nothing, pass to children instead
+        for (int i = children.size() - 1 ; i >= 0 ; i--) {
+            TWidget widget = children.get(i);
+            if (widget.mouseWouldHit(mouse)) {
+                // Dispatch to this child, also activate it
+                activate(widget);
+
+                // Set x and y relative to the child's coordinates
+                mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+                mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+                widget.onMouseDoubleClick(mouse);
+                return;
+            }
+        }
     }
 
     /**
-     * Get the Screen.
+     * Method that subclasses can override to handle window/screen resize
+     * events.
      *
-     * @return the Screen
+     * @param resize resize event
      */
-    public Screen getScreen() {
-        return window.getScreen();
+    public void onResize(final TResizeEvent resize) {
+        // Default: change my width/height.
+        if (resize.getType() == TResizeEvent.Type.WIDGET) {
+            width = resize.getWidth();
+            height = resize.getHeight();
+        } else {
+            // Let children see the screen resize
+            for (TWidget widget: children) {
+                widget.onResize(resize);
+            }
+        }
     }
 
     /**
-     * Comparison operator.  For various subclasses it sorts on:
-     * <ul>
-     * <li>tabOrder for TWidgets</li>
-     * <li>z for TWindows</li>
-     * <li>text for TTreeItems</li>
-     * </ul>
+     * Method that subclasses can override to handle posted command events.
      *
-     * @param that another TWidget, TWindow, or TTreeItem instance
-     * @return difference between this.tabOrder and that.tabOrder, or
-     * difference between this.z and that.z, or String.compareTo(text)
+     * @param command command event
      */
-    public final int compareTo(final TWidget that) {
-        if ((this instanceof TWindow)
-            && (that instanceof TWindow)
-        ) {
-            return (((TWindow) this).getZ() - ((TWindow) that).getZ());
-        }
-        if ((this instanceof TTreeItem)
-            && (that instanceof TTreeItem)
-        ) {
-            return (((TTreeItem) this).getText().compareTo(
-                ((TTreeItem) that).getText()));
+    public void onCommand(final TCommandEvent command) {
+        // Default: do nothing, pass to children instead
+        for (TWidget widget: children) {
+            widget.onCommand(command);
         }
-        return (this.tabOrder - that.tabOrder);
     }
 
     /**
-     * See if this widget should render with the active color.
+     * Method that subclasses can override to handle menu or posted menu
+     * events.
      *
-     * @return true if this widget is active and all of its parents are
-     * active.
+     * @param menu menu event
      */
-    public final boolean isAbsoluteActive() {
-        if (parent == this) {
-            return active;
+    public void onMenu(final TMenuEvent menu) {
+        // Default: do nothing, pass to children instead
+        for (TWidget widget: children) {
+            widget.onMenu(menu);
         }
-        return (active && parent.isAbsoluteActive());
     }
 
     /**
-     * Returns the cursor X position.
-     *
-     * @return absolute screen column number for the cursor's X position
+     * Method that subclasses can override to do processing when the UI is
+     * idle.  Note that repainting is NOT assumed.  To get a refresh after
+     * onIdle, call doRepaint().
      */
-    public final int getCursorAbsoluteX() {
-        return getAbsoluteX() + cursorX;
+    public void onIdle() {
+        // Default: do nothing, pass to children instead
+        for (TWidget widget: children) {
+            widget.onIdle();
+        }
     }
 
     /**
-     * Returns the cursor Y position.
+     * Consume event.  Subclasses that want to intercept all events in one go
+     * can override this method.
      *
-     * @return absolute screen row number for the cursor's Y position
+     * @param event keyboard, mouse, resize, command, or menu event
      */
-    public final int getCursorAbsoluteY() {
-        return getAbsoluteY() + cursorY;
+    public void handleEvent(final TInputEvent event) {
+        /*
+        System.err.printf("TWidget (%s) event: %s\n", this.getClass().getName(),
+            event);
+        */
+
+        if (!enabled) {
+            // Discard event
+            // System.err.println("   -- discard --");
+            return;
+        }
+
+        if (event instanceof TKeypressEvent) {
+            onKeypress((TKeypressEvent) event);
+        } else if (event instanceof TMouseEvent) {
+
+            TMouseEvent mouse = (TMouseEvent) event;
+
+            switch (mouse.getType()) {
+
+            case MOUSE_DOWN:
+                onMouseDown(mouse);
+                break;
+
+            case MOUSE_UP:
+                onMouseUp(mouse);
+                break;
+
+            case MOUSE_MOTION:
+                onMouseMotion(mouse);
+                break;
+
+            case MOUSE_DOUBLE_CLICK:
+                onMouseDoubleClick(mouse);
+                break;
+
+            default:
+                throw new IllegalArgumentException("Invalid mouse event type: "
+                    + mouse.getType());
+            }
+        } else if (event instanceof TResizeEvent) {
+            onResize((TResizeEvent) event);
+        } else if (event instanceof TCommandEvent) {
+            onCommand((TCommandEvent) event);
+        } else if (event instanceof TMenuEvent) {
+            onMenu((TMenuEvent) event);
+        }
+
+        // Do nothing else
+        return;
     }
 
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
-     * Compute my absolute X position as the sum of my X plus all my parent's
-     * X's.
+     * Get parent widget.
      *
-     * @return absolute screen column number for my X position
+     * @return parent widget
      */
-    public final int getAbsoluteX() {
-        assert (parent != null);
-        if (parent == this) {
-            return x;
-        }
-        if ((parent instanceof TWindow)
-            && !(parent instanceof TMenu)
-            && !(parent instanceof TDesktop)
-        ) {
-            // Widgets on a TWindow have (0,0) as their top-left, but this is
-            // actually the TWindow's (1,1).
-            return parent.getAbsoluteX() + x + 1;
-        }
-        return parent.getAbsoluteX() + x;
+    public final TWidget getParent() {
+        return parent;
     }
 
     /**
-     * Compute my absolute Y position as the sum of my Y plus all my parent's
-     * Y's.
+     * Get the list of child widgets that this widget contains.
      *
-     * @return absolute screen row number for my Y position
+     * @return the list of child widgets
      */
-    public final int getAbsoluteY() {
-        assert (parent != null);
-        if (parent == this) {
-            return y;
-        }
-        if ((parent instanceof TWindow)
-            && !(parent instanceof TMenu)
-            && !(parent instanceof TDesktop)
-        ) {
-            // Widgets on a TWindow have (0,0) as their top-left, but this is
-            // actually the TWindow's (1,1).
-            return parent.getAbsoluteY() + y + 1;
-        }
-        return parent.getAbsoluteY() + y;
+    public List<TWidget> getChildren() {
+        return children;
     }
 
     /**
-     * Get the global color theme.
+     * Get active flag.
      *
-     * @return the ColorTheme
+     * @return if true, this widget will receive events
      */
-    public final ColorTheme getTheme() {
-        return window.getApplication().getTheme();
+    public final boolean isActive() {
+        return active;
     }
 
     /**
-     * Draw my specific widget.  When called, the screen rectangle I draw
-     * into is already setup (offset and clipping).
+     * Set active flag.
+     *
+     * @param active if true, this widget will receive events
      */
-    public void draw() {
-        // Default widget draws nothing.
+    public final void setActive(final boolean active) {
+        this.active = active;
     }
 
     /**
-     * Called by parent to render to TWindow.
+     * Get the window this widget is on.
+     *
+     * @return the window
      */
-    public final void drawChildren() {
-        // Set my clipping rectangle
-        assert (window != null);
-        assert (getScreen() != null);
-        Screen screen = getScreen();
-
-        // Special case: TStatusBar is drawn by TApplication, not anything
-        // else.
-        if (this instanceof TStatusBar) {
-            return;
-        }
-
-        screen.setClipRight(width);
-        screen.setClipBottom(height);
-
-        int absoluteRightEdge = window.getAbsoluteX() + window.getWidth();
-        int absoluteBottomEdge = window.getAbsoluteY() + window.getHeight();
-        if (!(this instanceof TWindow) && !(this instanceof TVScroller)) {
-            absoluteRightEdge -= 1;
-        }
-        if (!(this instanceof TWindow) && !(this instanceof THScroller)) {
-            absoluteBottomEdge -= 1;
-        }
-        int myRightEdge = getAbsoluteX() + width;
-        int myBottomEdge = getAbsoluteY() + height;
-        if (getAbsoluteX() > absoluteRightEdge) {
-            // I am offscreen
-            screen.setClipRight(0);
-        } else if (myRightEdge > absoluteRightEdge) {
-            screen.setClipRight(screen.getClipRight()
-                - (myRightEdge - absoluteRightEdge));
-        }
-        if (getAbsoluteY() > absoluteBottomEdge) {
-            // I am offscreen
-            screen.setClipBottom(0);
-        } else if (myBottomEdge > absoluteBottomEdge) {
-            screen.setClipBottom(screen.getClipBottom()
-                - (myBottomEdge - absoluteBottomEdge));
-        }
-
-        // Set my offset
-        screen.setOffsetX(getAbsoluteX());
-        screen.setOffsetY(getAbsoluteY());
-
-        // Draw me
-        draw();
-
-        // Continue down the chain
-        for (TWidget widget: children) {
-            widget.drawChildren();
-        }
+    public final TWindow getWindow() {
+        return window;
     }
 
     /**
-     * Repaint the screen on the next update.
+     * Get X position.
+     *
+     * @return absolute X position of the top-left corner
      */
-    public void doRepaint() {
-        window.getApplication().doRepaint();
+    public final int getX() {
+        return x;
     }
 
-    // ------------------------------------------------------------------------
-    // Constructors -----------------------------------------------------------
-    // ------------------------------------------------------------------------
-
     /**
-     * Default constructor for subclasses.
+     * Set X position.
+     *
+     * @param x absolute X position of the top-left corner
      */
-    protected TWidget() {
-        children = new ArrayList<TWidget>();
+    public final void setX(final int x) {
+        this.x = x;
     }
 
     /**
-     * Protected constructor.
+     * Get Y position.
      *
-     * @param parent parent widget
+     * @return absolute Y position of the top-left corner
      */
-    protected TWidget(final TWidget parent) {
-        this(parent, true);
+    public final int getY() {
+        return y;
     }
 
     /**
-     * Protected constructor.
+     * Set Y position.
      *
-     * @param parent parent widget
-     * @param x column relative to parent
-     * @param y row relative to parent
-     * @param width width of widget
-     * @param height height of widget
+     * @param y absolute Y position of the top-left corner
      */
-    protected TWidget(final TWidget parent, final int x, final int y,
-        final int width, final int height) {
-
-        this(parent, true, x, y, width, height);
+    public final void setY(final int y) {
+        this.y = y;
     }
 
     /**
-     * Protected constructor used by subclasses that are disabled by default.
+     * Get the width.
      *
-     * @param parent parent widget
-     * @param enabled if true assume enabled
+     * @return widget width
      */
-    protected TWidget(final TWidget parent, final boolean enabled) {
-        this.enabled = enabled;
-        this.parent = parent;
-        this.window = parent.window;
-        children = new ArrayList<TWidget>();
-
-        // Do not add TStatusBars, they are drawn by TApplication
-        if (this instanceof TStatusBar) {
-        } else {
-            parent.addChild(this);
-        }
+    public final int getWidth() {
+        return this.width;
     }
 
     /**
-     * Protected constructor used by subclasses that are disabled by default.
+     * Change the width.
      *
-     * @param parent parent widget
-     * @param enabled if true assume enabled
-     * @param x column relative to parent
-     * @param y row relative to parent
-     * @param width width of widget
-     * @param height height of widget
+     * @param width new widget width
      */
-    protected TWidget(final TWidget parent, final boolean enabled,
-        final int x, final int y, final int width, final int height) {
-
-        this.enabled = enabled;
-        this.parent = parent;
-        this.window = parent.window;
-        children = new ArrayList<TWidget>();
-
-        // Do not add TStatusBars, they are drawn by TApplication
-        if (this instanceof TStatusBar) {
-        } else {
-            parent.addChild(this);
-        }
-
-        this.x = x;
-        this.y = y;
+    public final void setWidth(final int width) {
         this.width = width;
-        this.height = height;
     }
 
     /**
-     * Backdoor access for TWindow's constructor.  ONLY TWindow USES THIS.
+     * Get the height.
      *
-     * @param window the top-level window
-     * @param x column relative to parent
-     * @param y row relative to parent
-     * @param width width of window
-     * @param height height of window
+     * @return widget height
      */
-    protected final void setupForTWindow(final TWindow window,
-        final int x, final int y, final int width, final int height) {
+    public final int getHeight() {
+        return this.height;
+    }
 
-        this.parent = window;
-        this.window = window;
-        this.x      = x;
-        this.y      = y;
-        this.width  = width;
+    /**
+     * Change the height.
+     *
+     * @param height new widget height
+     */
+    public final void setHeight(final int height) {
         this.height = height;
     }
 
-    // ------------------------------------------------------------------------
-    // General behavior -------------------------------------------------------
-    // ------------------------------------------------------------------------
-
     /**
-     * Add a child widget to my list of children.  We set its tabOrder to 0
-     * and increment the tabOrder of all other children.
+     * Change the dimensions.
      *
-     * @param child TWidget to add
+     * @param x absolute X position of the top-left corner
+     * @param y absolute Y position of the top-left corner
+     * @param width new widget width
+     * @param height new widget height
      */
-    private void addChild(final TWidget child) {
-        children.add(child);
+    public final void setDimensions(final int x, final int y, final int width,
+        final int height) {
 
-        if ((child.enabled)
-            && !(child instanceof THScroller)
-            && !(child instanceof TVScroller)
-        ) {
-            for (TWidget widget: children) {
-                widget.active = false;
-            }
-            child.active = true;
-            activeChild = child;
-        }
-        for (int i = 0; i < children.size(); i++) {
-            children.get(i).tabOrder = i;
-        }
+        setX(x);
+        setY(y);
+        setWidth(width);
+        setHeight(height);
     }
 
     /**
-     * Switch the active child.
+     * Get enabled flag.
      *
-     * @param child TWidget to activate
+     * @return if true, this widget can be tabbed to or receive events
      */
-    public final void activate(final TWidget child) {
-        assert (child.enabled);
-        if ((child instanceof THScroller)
-            || (child instanceof TVScroller)
-        ) {
-            return;
-        }
+    public final boolean isEnabled() {
+        return enabled;
+    }
 
-        if (child != activeChild) {
-            if (activeChild != null) {
-                activeChild.active = false;
+    /**
+     * Set enabled flag.
+     *
+     * @param enabled if true, this widget can be tabbed to or receive events
+     */
+    public final void setEnabled(final boolean enabled) {
+        this.enabled = enabled;
+        if (!enabled) {
+            active = false;
+            // See if there are any active siblings to switch to
+            boolean foundSibling = false;
+            if (parent != null) {
+                for (TWidget w: parent.children) {
+                    if ((w.enabled)
+                        && !(this instanceof THScroller)
+                        && !(this instanceof TVScroller)
+                    ) {
+                        parent.activate(w);
+                        foundSibling = true;
+                        break;
+                    }
+                }
+                if (!foundSibling) {
+                    parent.activeChild = null;
+                }
             }
-            child.active = true;
-            activeChild = child;
         }
     }
 
     /**
-     * Switch the active child.
+     * Set visible cursor flag.
      *
-     * @param tabOrder tabOrder of the child to activate.  If that child
-     * isn't enabled, then the next enabled child will be activated.
+     * @param cursorVisible if true, this widget has a cursor
      */
-    public final void activate(final int tabOrder) {
-        if (activeChild == null) {
-            return;
-        }
-        TWidget child = null;
-        for (TWidget widget: children) {
-            if ((widget.enabled)
-                && !(widget instanceof THScroller)
-                && !(widget instanceof TVScroller)
-                && (widget.tabOrder >= tabOrder)
-            ) {
-                child = widget;
-                break;
-            }
-        }
-        if ((child != null) && (child != activeChild)) {
-            activeChild.active = false;
-            assert (child.enabled);
-            child.active = true;
-            activeChild = child;
-        }
+    public final void setCursorVisible(final boolean cursorVisible) {
+        this.cursorVisible = cursorVisible;
     }
 
     /**
-     * Switch the active widget with the next in the tab order.
+     * See if this widget has a visible cursor.
      *
-     * @param forward if true, then switch to the next enabled widget in the
-     * list, otherwise switch to the previous enabled widget in the list
+     * @return if true, this widget has a visible cursor
      */
-    public final void switchWidget(final boolean forward) {
+    public final boolean isCursorVisible() {
+        // If cursor is out of my bounds, it is not visible.
+        if ((cursorX >= width)
+            || (cursorX < 0)
+            || (cursorY >= height)
+            || (cursorY < 0)
+        ) {
+            return false;
+        }
 
-        // Only switch if there are multiple enabled widgets
-        if ((children.size() < 2) || (activeChild == null)) {
-            return;
+        // If cursor is out of my window's bounds, it is not visible.
+        if ((getCursorAbsoluteX() >= window.getAbsoluteX()
+                + window.getWidth() - 1)
+            || (getCursorAbsoluteX() < 0)
+            || (getCursorAbsoluteY() >= window.getAbsoluteY()
+                + window.getHeight() - 1)
+            || (getCursorAbsoluteY() < 0)
+        ) {
+            return false;
         }
+        return cursorVisible;
+    }
 
-        int tabOrder = activeChild.tabOrder;
-        do {
-            if (forward) {
-                tabOrder++;
-            } else {
-                tabOrder--;
-            }
-            if (tabOrder < 0) {
+    /**
+     * Get cursor X value.
+     *
+     * @return cursor column position in relative coordinates
+     */
+    public final int getCursorX() {
+        return cursorX;
+    }
 
-                // If at the end, pass the switch to my parent.
-                if ((!forward) && (parent != this)) {
-                    parent.switchWidget(forward);
-                    return;
-                }
+    /**
+     * Set cursor X value.
+     *
+     * @param cursorX column position in relative coordinates
+     */
+    public final void setCursorX(final int cursorX) {
+        this.cursorX = cursorX;
+    }
 
-                tabOrder = children.size() - 1;
-            } else if (tabOrder == children.size()) {
-                // If at the end, pass the switch to my parent.
-                if ((forward) && (parent != this)) {
-                    parent.switchWidget(forward);
-                    return;
-                }
+    /**
+     * Get cursor Y value.
+     *
+     * @return cursor row position in relative coordinates
+     */
+    public final int getCursorY() {
+        return cursorY;
+    }
 
-                tabOrder = 0;
-            }
-            if (activeChild.tabOrder == tabOrder) {
-                // We wrapped around
-                break;
-            }
-        } while ((!children.get(tabOrder).enabled)
-            && !(children.get(tabOrder) instanceof THScroller)
-            && !(children.get(tabOrder) instanceof TVScroller));
+    /**
+     * Set cursor Y value.
+     *
+     * @param cursorY row position in relative coordinates
+     */
+    public final void setCursorY(final int cursorY) {
+        this.cursorY = cursorY;
+    }
 
-        assert (children.get(tabOrder).enabled);
+    /**
+     * Get this TWidget's parent TApplication.
+     *
+     * @return the parent TApplication
+     */
+    public TApplication getApplication() {
+        return window.getApplication();
+    }
 
-        activeChild.active = false;
-        children.get(tabOrder).active = true;
-        activeChild = children.get(tabOrder);
+    /**
+     * Get the Screen.
+     *
+     * @return the Screen
+     */
+    public Screen getScreen() {
+        return window.getScreen();
     }
 
     /**
-     * Returns my active widget.
+     * Comparison operator.  For various subclasses it sorts on:
+     * <ul>
+     * <li>tabOrder for TWidgets</li>
+     * <li>z for TWindows</li>
+     * <li>text for TTreeItems</li>
+     * </ul>
      *
-     * @return widget that is active, or this if no children
+     * @param that another TWidget, TWindow, or TTreeItem instance
+     * @return difference between this.tabOrder and that.tabOrder, or
+     * difference between this.z and that.z, or String.compareTo(text)
      */
-    public TWidget getActiveChild() {
-        if ((this instanceof THScroller)
-            || (this instanceof TVScroller)
+    public final int compareTo(final TWidget that) {
+        if ((this instanceof TWindow)
+            && (that instanceof TWindow)
         ) {
-            return parent;
+            return (((TWindow) this).getZ() - ((TWindow) that).getZ());
+        }
+        if ((this instanceof TTreeItem)
+            && (that instanceof TTreeItem)
+        ) {
+            return (((TTreeItem) this).getText().compareTo(
+                ((TTreeItem) that).getText()));
         }
+        return (this.tabOrder - that.tabOrder);
+    }
 
-        for (TWidget widget: children) {
-            if (widget.active) {
-                return widget.getActiveChild();
-            }
+    /**
+     * See if this widget should render with the active color.
+     *
+     * @return true if this widget is active and all of its parents are
+     * active.
+     */
+    public final boolean isAbsoluteActive() {
+        if (parent == this) {
+            return active;
         }
-        // No active children, return me
-        return this;
+        return (active && parent.isAbsoluteActive());
     }
 
-    // ------------------------------------------------------------------------
-    // Event handlers ---------------------------------------------------------
-    // ------------------------------------------------------------------------
+    /**
+     * Returns the cursor X position.
+     *
+     * @return absolute screen column number for the cursor's X position
+     */
+    public final int getCursorAbsoluteX() {
+        return getAbsoluteX() + cursorX;
+    }
 
     /**
-     * Check if a mouse press/release event coordinate is contained in this
-     * widget.
+     * Returns the cursor Y position.
      *
-     * @param mouse a mouse-based event
-     * @return whether or not a mouse click would be sent to this widget
+     * @return absolute screen row number for the cursor's Y position
      */
-    public final boolean mouseWouldHit(final TMouseEvent mouse) {
+    public final int getCursorAbsoluteY() {
+        return getAbsoluteY() + cursorY;
+    }
 
-        if (!enabled) {
-            return false;
+    /**
+     * Compute my absolute X position as the sum of my X plus all my parent's
+     * X's.
+     *
+     * @return absolute screen column number for my X position
+     */
+    public final int getAbsoluteX() {
+        assert (parent != null);
+        if (parent == this) {
+            return x;
         }
+        if ((parent instanceof TWindow)
+            && !(parent instanceof TMenu)
+            && !(parent instanceof TDesktop)
+        ) {
+            // Widgets on a TWindow have (0,0) as their top-left, but this is
+            // actually the TWindow's (1,1).
+            return parent.getAbsoluteX() + x + 1;
+        }
+        return parent.getAbsoluteX() + x;
+    }
 
-        if ((mouse.getAbsoluteX() >= getAbsoluteX())
-            && (mouse.getAbsoluteX() <  getAbsoluteX() + width)
-            && (mouse.getAbsoluteY() >= getAbsoluteY())
-            && (mouse.getAbsoluteY() <  getAbsoluteY() + height)
+    /**
+     * Compute my absolute Y position as the sum of my Y plus all my parent's
+     * Y's.
+     *
+     * @return absolute screen row number for my Y position
+     */
+    public final int getAbsoluteY() {
+        assert (parent != null);
+        if (parent == this) {
+            return y;
+        }
+        if ((parent instanceof TWindow)
+            && !(parent instanceof TMenu)
+            && !(parent instanceof TDesktop)
         ) {
-            return true;
+            // Widgets on a TWindow have (0,0) as their top-left, but this is
+            // actually the TWindow's (1,1).
+            return parent.getAbsoluteY() + y + 1;
         }
-        return false;
+        return parent.getAbsoluteY() + y;
     }
 
     /**
-     * Method that subclasses can override to handle keystrokes.
+     * Get the global color theme.
      *
-     * @param keypress keystroke event
+     * @return the ColorTheme
      */
-    public void onKeypress(final TKeypressEvent keypress) {
+    public final ColorTheme getTheme() {
+        return window.getApplication().getTheme();
+    }
 
-        if ((children.size() == 0)
-            || (this instanceof TTreeView)
-            || (this instanceof TText)
-        ) {
+    /**
+     * Draw my specific widget.  When called, the screen rectangle I draw
+     * into is already setup (offset and clipping).
+     */
+    public void draw() {
+        // Default widget draws nothing.
+    }
 
-            // Defaults:
-            //   tab / shift-tab - switch to next/previous widget
-            //   right-arrow or down-arrow: same as tab
-            //   left-arrow or up-arrow: same as shift-tab
-            if ((keypress.equals(kbTab))
-                || (keypress.equals(kbRight))
-                || (keypress.equals(kbDown))
-            ) {
-                parent.switchWidget(true);
-                return;
-            } else if ((keypress.equals(kbShiftTab))
-                || (keypress.equals(kbBackTab))
-                || (keypress.equals(kbLeft))
-                || (keypress.equals(kbUp))
-            ) {
-                parent.switchWidget(false);
-                return;
-            }
+    /**
+     * Called by parent to render to TWindow.
+     */
+    public final void drawChildren() {
+        // Set my clipping rectangle
+        assert (window != null);
+        assert (getScreen() != null);
+        Screen screen = getScreen();
+
+        // Special case: TStatusBar is drawn by TApplication, not anything
+        // else.
+        if (this instanceof TStatusBar) {
+            return;
         }
 
-        // If I have any buttons on me AND this is an Alt-key that matches
-        // its mnemonic, send it an Enter keystroke
-        for (TWidget widget: children) {
-            if (widget instanceof TButton) {
-                TButton button = (TButton) widget;
-                if (button.isEnabled()
-                    && !keypress.getKey().isFnKey()
-                    && keypress.getKey().isAlt()
-                    && !keypress.getKey().isCtrl()
-                    && (Character.toLowerCase(button.getMnemonic().getShortcut())
-                        == Character.toLowerCase(keypress.getKey().getChar()))
-                ) {
+        screen.setClipRight(width);
+        screen.setClipBottom(height);
 
-                    widget.handleEvent(new TKeypressEvent(kbEnter));
-                    return;
-                }
-            }
+        int absoluteRightEdge = window.getAbsoluteX() + window.getWidth();
+        int absoluteBottomEdge = window.getAbsoluteY() + window.getHeight();
+        if (!(this instanceof TWindow) && !(this instanceof TVScroller)) {
+            absoluteRightEdge -= 1;
+        }
+        if (!(this instanceof TWindow) && !(this instanceof THScroller)) {
+            absoluteBottomEdge -= 1;
         }
+        int myRightEdge = getAbsoluteX() + width;
+        int myBottomEdge = getAbsoluteY() + height;
+        if (getAbsoluteX() > absoluteRightEdge) {
+            // I am offscreen
+            screen.setClipRight(0);
+        } else if (myRightEdge > absoluteRightEdge) {
+            screen.setClipRight(screen.getClipRight()
+                - (myRightEdge - absoluteRightEdge));
+        }
+        if (getAbsoluteY() > absoluteBottomEdge) {
+            // I am offscreen
+            screen.setClipBottom(0);
+        } else if (myBottomEdge > absoluteBottomEdge) {
+            screen.setClipBottom(screen.getClipBottom()
+                - (myBottomEdge - absoluteBottomEdge));
+        }
+
+        // Set my offset
+        screen.setOffsetX(getAbsoluteX());
+        screen.setOffsetY(getAbsoluteY());
 
-        // Dispatch the keypress to an active widget
+        // Draw me
+        draw();
+
+        // Continue down the chain
         for (TWidget widget: children) {
-            if (widget.active) {
-                widget.handleEvent(keypress);
-                return;
-            }
+            widget.drawChildren();
         }
     }
 
     /**
-     * Method that subclasses can override to handle mouse button presses.
-     *
-     * @param mouse mouse button event
+     * Repaint the screen on the next update.
      */
-    public void onMouseDown(final TMouseEvent mouse) {
-        // Default: do nothing, pass to children instead
-        for (int i = children.size() - 1 ; i >= 0 ; i--) {
-            TWidget widget = children.get(i);
-            if (widget.mouseWouldHit(mouse)) {
-                // Dispatch to this child, also activate it
-                activate(widget);
-
-                // Set x and y relative to the child's coordinates
-                mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
-                mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
-                widget.handleEvent(mouse);
-                return;
-            }
-        }
+    public void doRepaint() {
+        window.getApplication().doRepaint();
     }
 
     /**
-     * Method that subclasses can override to handle mouse button releases.
+     * Add a child widget to my list of children.  We set its tabOrder to 0
+     * and increment the tabOrder of all other children.
      *
-     * @param mouse mouse button event
+     * @param child TWidget to add
      */
-    public void onMouseUp(final TMouseEvent mouse) {
-        // Default: do nothing, pass to children instead
-        for (int i = children.size() - 1 ; i >= 0 ; i--) {
-            TWidget widget = children.get(i);
-            if (widget.mouseWouldHit(mouse)) {
-                // Dispatch to this child, also activate it
-                activate(widget);
+    private void addChild(final TWidget child) {
+        children.add(child);
 
-                // Set x and y relative to the child's coordinates
-                mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
-                mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
-                widget.handleEvent(mouse);
-                return;
+        if ((child.enabled)
+            && !(child instanceof THScroller)
+            && !(child instanceof TVScroller)
+        ) {
+            for (TWidget widget: children) {
+                widget.active = false;
             }
+            child.active = true;
+            activeChild = child;
         }
-    }
-
-    /**
-     * Method that subclasses can override to handle mouse movements.
-     *
-     * @param mouse mouse motion event
-     */
-    public void onMouseMotion(final TMouseEvent mouse) {
-        // Default: do nothing, pass it on to ALL of my children.  This way
-        // the children can see the mouse "leaving" their area.
-        for (TWidget widget: children) {
-            // Set x and y relative to the child's coordinates
-            mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
-            mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
-            widget.handleEvent(mouse);
+        for (int i = 0; i < children.size(); i++) {
+            children.get(i).tabOrder = i;
         }
     }
 
     /**
-     * Method that subclasses can override to handle mouse button
-     * double-clicks.
+     * Switch the active child.
      *
-     * @param mouse mouse button event
+     * @param child TWidget to activate
      */
-    public void onMouseDoubleClick(final TMouseEvent mouse) {
-        // Default: do nothing, pass to children instead
-        for (int i = children.size() - 1 ; i >= 0 ; i--) {
-            TWidget widget = children.get(i);
-            if (widget.mouseWouldHit(mouse)) {
-                // Dispatch to this child, also activate it
-                activate(widget);
-
-                // Set x and y relative to the child's coordinates
-                mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
-                mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
-                widget.handleEvent(mouse);
-                return;
-            }
+    public final void activate(final TWidget child) {
+        assert (child.enabled);
+        if ((child instanceof THScroller)
+            || (child instanceof TVScroller)
+        ) {
+            return;
         }
-    }
 
-    /**
-     * Method that subclasses can override to handle window/screen resize
-     * events.
-     *
-     * @param resize resize event
-     */
-    public void onResize(final TResizeEvent resize) {
-        // Default: change my width/height.
-        if (resize.getType() == TResizeEvent.Type.WIDGET) {
-            width = resize.getWidth();
-            height = resize.getHeight();
-        } else {
-            // Let children see the screen resize
-            for (TWidget widget: children) {
-                widget.onResize(resize);
+        if (child != activeChild) {
+            if (activeChild != null) {
+                activeChild.active = false;
             }
+            child.active = true;
+            activeChild = child;
         }
     }
 
     /**
-     * Method that subclasses can override to handle posted command events.
+     * Switch the active child.
      *
-     * @param command command event
+     * @param tabOrder tabOrder of the child to activate.  If that child
+     * isn't enabled, then the next enabled child will be activated.
      */
-    public void onCommand(final TCommandEvent command) {
-        // Default: do nothing, pass to children instead
-        for (TWidget widget: children) {
-            widget.onCommand(command);
+    public final void activate(final int tabOrder) {
+        if (activeChild == null) {
+            return;
         }
-    }
-
-    /**
-     * Method that subclasses can override to handle menu or posted menu
-     * events.
-     *
-     * @param menu menu event
-     */
-    public void onMenu(final TMenuEvent menu) {
-        // Default: do nothing, pass to children instead
+        TWidget child = null;
         for (TWidget widget: children) {
-            widget.onMenu(menu);
+            if ((widget.enabled)
+                && !(widget instanceof THScroller)
+                && !(widget instanceof TVScroller)
+                && (widget.tabOrder >= tabOrder)
+            ) {
+                child = widget;
+                break;
+            }
         }
-    }
-
-    /**
-     * Method that subclasses can override to do processing when the UI is
-     * idle.  Note that repainting is NOT assumed.  To get a refresh after
-     * onIdle, call doRepaint().
-     */
-    public void onIdle() {
-        // Default: do nothing, pass to children instead
-        for (TWidget widget: children) {
-            widget.onIdle();
+        if ((child != null) && (child != activeChild)) {
+            activeChild.active = false;
+            assert (child.enabled);
+            child.active = true;
+            activeChild = child;
         }
     }
 
     /**
-     * Consume event.  Subclasses that want to intercept all events in one go
-     * can override this method.
+     * Switch the active widget with the next in the tab order.
      *
-     * @param event keyboard, mouse, resize, command, or menu event
+     * @param forward if true, then switch to the next enabled widget in the
+     * list, otherwise switch to the previous enabled widget in the list
      */
-    public void handleEvent(final TInputEvent event) {
-        // System.err.printf("TWidget (%s) event: %s\n", this.getClass().getName(),
-        //     event);
+    public final void switchWidget(final boolean forward) {
 
-        if (!enabled) {
-            // Discard event
-            // System.err.println("   -- discard --");
+        // Only switch if there are multiple enabled widgets
+        if ((children.size() < 2) || (activeChild == null)) {
             return;
         }
 
-        if (event instanceof TKeypressEvent) {
-            onKeypress((TKeypressEvent) event);
-        } else if (event instanceof TMouseEvent) {
+        int tabOrder = activeChild.tabOrder;
+        do {
+            if (forward) {
+                tabOrder++;
+            } else {
+                tabOrder--;
+            }
+            if (tabOrder < 0) {
 
-            TMouseEvent mouse = (TMouseEvent) event;
+                // If at the end, pass the switch to my parent.
+                if ((!forward) && (parent != this)) {
+                    parent.switchWidget(forward);
+                    return;
+                }
 
-            switch (mouse.getType()) {
+                tabOrder = children.size() - 1;
+            } else if (tabOrder == children.size()) {
+                // If at the end, pass the switch to my parent.
+                if ((forward) && (parent != this)) {
+                    parent.switchWidget(forward);
+                    return;
+                }
 
-            case MOUSE_DOWN:
-                onMouseDown(mouse);
+                tabOrder = 0;
+            }
+            if (activeChild.tabOrder == tabOrder) {
+                // We wrapped around
                 break;
+            }
+        } while ((!children.get(tabOrder).enabled)
+            && !(children.get(tabOrder) instanceof THScroller)
+            && !(children.get(tabOrder) instanceof TVScroller));
 
-            case MOUSE_UP:
-                onMouseUp(mouse);
-                break;
+        assert (children.get(tabOrder).enabled);
 
-            case MOUSE_MOTION:
-                onMouseMotion(mouse);
-                break;
+        activeChild.active = false;
+        children.get(tabOrder).active = true;
+        activeChild = children.get(tabOrder);
+    }
 
-            case MOUSE_DOUBLE_CLICK:
-                onMouseDoubleClick(mouse);
-                break;
+    /**
+     * Returns my active widget.
+     *
+     * @return widget that is active, or this if no children
+     */
+    public TWidget getActiveChild() {
+        if ((this instanceof THScroller)
+            || (this instanceof TVScroller)
+        ) {
+            return parent;
+        }
 
-            default:
-                throw new IllegalArgumentException("Invalid mouse event type: "
-                    + mouse.getType());
+        for (TWidget widget: children) {
+            if (widget.active) {
+                return widget.getActiveChild();
             }
-        } else if (event instanceof TResizeEvent) {
-            onResize((TResizeEvent) event);
-        } else if (event instanceof TCommandEvent) {
-            onCommand((TCommandEvent) event);
-        } else if (event instanceof TMenuEvent) {
-            onMenu((TMenuEvent) event);
         }
-
-        // Do nothing else
-        return;
+        // No active children, return me
+        return this;
     }
 
     // ------------------------------------------------------------------------
@@ -1421,7 +1440,8 @@ public abstract class TWidget implements Comparable<TWidget> {
     }
 
     /**
-     * Convenience function to add a tree view to this container/window.
+     * Convenience function to add a scrollable tree view to this
+     * container/window.
      *
      * @param x column relative to parent
      * @param y row relative to parent
@@ -1429,14 +1449,15 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @param height height of tree view
      * @return the new tree view
      */
-    public final TTreeView addTreeView(final int x, final int y,
+    public final TTreeViewWidget addTreeViewWidget(final int x, final int y,
         final int width, final int height) {
 
-        return new TTreeView(this, x, y, width, height);
+        return new TTreeViewWidget(this, x, y, width, height);
     }
 
     /**
-     * Convenience function to add a tree view to this container/window.
+     * Convenience function to add a scrollable tree view to this
+     * container/window.
      *
      * @param x column relative to parent
      * @param y row relative to parent
@@ -1445,10 +1466,10 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @param action action to perform when an item is selected
      * @return the new tree view
      */
-    public final TTreeView addTreeView(final int x, final int y,
+    public final TTreeViewWidget addTreeViewWidget(final int x, final int y,
         final int width, final int height, final TAction action) {
 
-        return new TTreeView(this, x, y, width, height, action);
+        return new TTreeViewWidget(this, x, y, width, height, action);
     }
 
     /**
index 19c96fd141d2550209a9ca8fb14ae53bcbfbc378..140a38aa263f1c2138aed7ff558ada47df9507e1 100644 (file)
@@ -29,6 +29,7 @@
 package jexer;
 
 import java.util.HashSet;
+import java.util.Set;
 
 import jexer.backend.Screen;
 import jexer.bits.Cell;
@@ -49,7 +50,7 @@ import static jexer.TKeypress.*;
 public class TWindow extends TWidget {
 
     // ------------------------------------------------------------------------
-    // Public constants -------------------------------------------------------
+    // Constants --------------------------------------------------------------
     // ------------------------------------------------------------------------
 
     /**
@@ -78,162 +79,48 @@ public class TWindow extends TWidget {
      */
     public static final int NOZOOMBOX   = 0x10;
 
-    // ------------------------------------------------------------------------
-    // Common window attributes -----------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Window flags.  Note package private access.
-     */
-    int flags = RESIZABLE;
-
     /**
-     * Window title.
-     */
-    private String title = "";
-
-    /**
-     * Get window title.
-     *
-     * @return window title
+     * Window is placed at absolute position (no smart placement) (default
+     * no).
      */
-    public final String getTitle() {
-        return title;
-    }
+    public static final int ABSOLUTEXY  = 0x20;
 
     /**
-     * Set window title.
-     *
-     * @param title new window title
+     * Hitting the closebox with the mouse calls TApplication.hideWindow()
+     * rather than TApplication.closeWindow() (default no).
      */
-    public final void setTitle(final String title) {
-        this.title = title;
-    }
+    public static final int HIDEONCLOSE = 0x40;
 
     // ------------------------------------------------------------------------
-    // TApplication integration -----------------------------------------------
+    // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
 
     /**
-     * Window's parent TApplication.
+     * Window flags.  Note package private access.
      */
-    private TApplication application;
+    int flags = RESIZABLE;
 
     /**
-     * Get this TWindow's parent TApplication.
-     *
-     * @return this TWindow's parent TApplication
+     * Window title.
      */
-    @Override
-    public final TApplication getApplication() {
-        return application;
-    }
+    private String title = "";
 
     /**
-     * Get the Screen.
-     *
-     * @return the Screen
+     * Window's parent TApplication.
      */
-    @Override
-    public final Screen getScreen() {
-        return application.getScreen();
-    }
+    private TApplication application;
 
     /**
      * Z order.  Lower number means more in-front.
      */
     private int z = 0;
 
-    /**
-     * Get Z order.  Lower number means more in-front.
-     *
-     * @return Z value.  Lower number means more in-front.
-     */
-    public final int getZ() {
-        return z;
-    }
-
-    /**
-     * Set Z order.  Lower number means more in-front.
-     *
-     * @param z the new Z value.  Lower number means more in-front.
-     */
-    public final void setZ(final int z) {
-        this.z = z;
-    }
-
     /**
      * Window's keyboard shortcuts.  Any key in this set will be passed to
      * the window directly rather than processed through the menu
      * accelerators.
      */
-    private HashSet<TKeypress> keyboardShortcuts = new HashSet<TKeypress>();
-
-    /**
-     * Add a keypress to be overridden for this window.
-     *
-     * @param key the key to start taking control of
-     */
-    protected void addShortcutKeypress(final TKeypress key) {
-        keyboardShortcuts.add(key);
-    }
-
-    /**
-     * Remove a keypress to be overridden for this window.
-     *
-     * @param key the key to stop taking control of
-     */
-    protected void removeShortcutKeypress(final TKeypress key) {
-        keyboardShortcuts.remove(key);
-    }
-
-    /**
-     * Remove all keypresses to be overridden for this window.
-     */
-    protected void clearShortcutKeypresses() {
-        keyboardShortcuts.clear();
-    }
-
-    /**
-     * Determine if a keypress is overridden for this window.
-     *
-     * @param key the key to check
-     * @return true if this window wants to process this key on its own
-     */
-    public boolean isShortcutKeypress(final TKeypress key) {
-        return keyboardShortcuts.contains(key);
-    }
-
-    /**
-     * A window may have a status bar associated with it.  TApplication will
-     * draw this status bar last, and will also route events to it first
-     * before the window.
-     */
-    protected TStatusBar statusBar = null;
-
-    /**
-     * Get the window's status bar, or null if it does not have one.
-     *
-     * @return the status bar, or null
-     */
-    public TStatusBar getStatusBar() {
-        return statusBar;
-    }
-
-    /**
-     * Set the window's status bar to a new one.
-     *
-     * @param text the status bar text
-     * @return the status bar
-     */
-    public TStatusBar newStatusBar(final String text) {
-        statusBar = new TStatusBar(this, text);
-        return statusBar;
-    }
-
-    // ------------------------------------------------------------------------
-    // Window movement/resizing support ---------------------------------------
-    // ------------------------------------------------------------------------
+    private Set<TKeypress> keyboardShortcuts = new HashSet<TKeypress>();
 
     /**
      * If true, then the user clicked on the title bar and is moving the
@@ -251,7 +138,7 @@ public class TWindow extends TWidget {
      * If true, then the user selected "Size/Move" (or hit Ctrl-F5) and is
      * resizing/moving the window via the keyboard.
      */
-    private boolean inKeyboardResize = false;
+    protected boolean inKeyboardResize = false;
 
     /**
      * If true, this window is maximized.
@@ -284,226 +171,67 @@ public class TWindow extends TWidget {
     private int restoreWindowY;
 
     /**
-     * Set the maximum width for this window.
-     *
-     * @param maximumWindowWidth new maximum width
+     * Hidden flag.  A hidden window will still have its onIdle() called, and
+     * will also have onClose() called at application exit.  Note package
+     * private access: TApplication will force hidden false if a modal window
+     * is active.
      */
-    public final void setMaximumWindowWidth(final int maximumWindowWidth) {
-        if ((maximumWindowWidth != -1)
-            && (maximumWindowWidth < minimumWindowWidth + 1)
-        ) {
-            throw new IllegalArgumentException("Maximum window width cannot " +
-                "be smaller than minimum window width + 1");
-        }
-        this.maximumWindowWidth = maximumWindowWidth;
-    }
+    boolean hidden = false;
 
     /**
-     * Set the minimum width for this window.
-     *
-     * @param minimumWindowWidth new minimum width
+     * A window may have a status bar associated with it.  TApplication will
+     * draw this status bar last, and will also route events to it first
+     * before the window.
      */
-    public final void setMinimumWindowWidth(final int minimumWindowWidth) {
-        if ((maximumWindowWidth != -1)
-            && (minimumWindowWidth > maximumWindowWidth - 1)
-        ) {
-            throw new IllegalArgumentException("Minimum window width cannot " +
-                "be larger than maximum window width - 1");
-        }
-        this.minimumWindowWidth = minimumWindowWidth;
-    }
+    protected TStatusBar statusBar = null;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
-     * Set the maximum height for this window.
+     * Public constructor.  Window will be located at (0, 0).
      *
-     * @param maximumWindowHeight new maximum height
+     * @param application TApplication that manages this window
+     * @param title window title, will be centered along the top border
+     * @param width width of window
+     * @param height height of window
      */
-    public final void setMaximumWindowHeight(final int maximumWindowHeight) {
-        if ((maximumWindowHeight != -1)
-            && (maximumWindowHeight < minimumWindowHeight + 1)
-        ) {
-            throw new IllegalArgumentException("Maximum window height cannot " +
-                "be smaller than minimum window height + 1");
-        }
-        this.maximumWindowHeight = maximumWindowHeight;
+    public TWindow(final TApplication application, final String title,
+        final int width, final int height) {
+
+        this(application, title, 0, 0, width, height, RESIZABLE);
     }
 
     /**
-     * Set the minimum height for this window.
+     * Public constructor.  Window will be located at (0, 0).
      *
-     * @param minimumWindowHeight new minimum height
+     * @param application TApplication that manages this window
+     * @param title window title, will be centered along the top border
+     * @param width width of window
+     * @param height height of window
+     * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
      */
-    public final void setMinimumWindowHeight(final int minimumWindowHeight) {
-        if ((maximumWindowHeight != -1)
-            && (minimumWindowHeight > maximumWindowHeight - 1)
-        ) {
-            throw new IllegalArgumentException("Minimum window height cannot " +
-                "be larger than maximum window height - 1");
-        }
-        this.minimumWindowHeight = minimumWindowHeight;
-    }
+    public TWindow(final TApplication application, final String title,
+        final int width, final int height, final int flags) {
 
-    /**
-     * Recenter the window on-screen.
-     */
-    public final void center() {
-        if ((flags & CENTERED) != 0) {
-            if (getWidth() < getScreen().getWidth()) {
-                setX((getScreen().getWidth() - getWidth()) / 2);
-            } else {
-                setX(0);
-            }
-            setY(((application.getDesktopBottom()
-                    - application.getDesktopTop()) - getHeight()) / 2);
-            if (getY() < 0) {
-                setY(0);
-            }
-            setY(getY() + application.getDesktopTop());
-        }
+        this(application, title, 0, 0, width, height, flags);
     }
 
     /**
-     * Maximize window.
+     * Public constructor.
+     *
+     * @param application TApplication that manages this window
+     * @param title window title, will be centered along the top border
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of window
+     * @param height height of window
      */
-    public void maximize() {
-        if (maximized) {
-            return;
-        }
+    public TWindow(final TApplication application, final String title,
+        final int x, final int y, final int width, final int height) {
 
-        restoreWindowWidth = getWidth();
-        restoreWindowHeight = getHeight();
-        restoreWindowX = getX();
-        restoreWindowY = getY();
-        setWidth(getScreen().getWidth());
-        setHeight(application.getDesktopBottom() - 1);
-        setX(0);
-        setY(1);
-        maximized = true;
-    }
-
-    /**
-     * Restore (unmaximize) window.
-     */
-    public void restore() {
-        if (!maximized) {
-            return;
-        }
-
-        setWidth(restoreWindowWidth);
-        setHeight(restoreWindowHeight);
-        setX(restoreWindowX);
-        setY(restoreWindowY);
-        maximized = false;
-    }
-
-    // ------------------------------------------------------------------------
-    // Window visibility ------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Hidden flag.  A hidden window will still have its onIdle() called, and
-     * will also have onClose() called at application exit.  Note package
-     * private access: TApplication will force hidden false if a modal window
-     * is active.
-     */
-    boolean hidden = false;
-
-    /**
-     * Returns true if this window is hidden.
-     *
-     * @return true if this window is hidden, false if the window is shown
-     */
-    public final boolean isHidden() {
-        return hidden;
-    }
-
-    /**
-     * Returns true if this window is shown.
-     *
-     * @return true if this window is shown, false if the window is hidden
-     */
-    public final boolean isShown() {
-        return !hidden;
-    }
-
-    /**
-     * Hide window.  A hidden window will still have its onIdle() called, and
-     * will also have onClose() called at application exit.  Hidden windows
-     * will not receive any other events.
-     */
-    public void hide() {
-        application.hideWindow(this);
-    }
-
-    /**
-     * Show window.
-     */
-    public void show() {
-        application.showWindow(this);
-    }
-
-    /**
-     * Activate window (bring to top and receive events).
-     */
-    public void activate() {
-        application.activateWindow(this);
-    }
-
-    /**
-     * Close window.  Note that windows without a close box can still be
-     * closed by calling the close() method.
-     */
-    public void close() {
-        application.closeWindow(this);
-    }
-
-    // ------------------------------------------------------------------------
-    // Constructors -----------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Public constructor.  Window will be located at (0, 0).
-     *
-     * @param application TApplication that manages this window
-     * @param title window title, will be centered along the top border
-     * @param width width of window
-     * @param height height of window
-     */
-    public TWindow(final TApplication application, final String title,
-        final int width, final int height) {
-
-        this(application, title, 0, 0, width, height, RESIZABLE);
-    }
-
-    /**
-     * Public constructor.  Window will be located at (0, 0).
-     *
-     * @param application TApplication that manages this window
-     * @param title window title, will be centered along the top border
-     * @param width width of window
-     * @param height height of window
-     * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
-     */
-    public TWindow(final TApplication application, final String title,
-        final int width, final int height, final int flags) {
-
-        this(application, title, 0, 0, width, height, flags);
-    }
-
-    /**
-     * Public constructor.
-     *
-     * @param application TApplication that manages this window
-     * @param title window title, will be centered along the top border
-     * @param x column relative to parent
-     * @param y row relative to parent
-     * @param width width of window
-     * @param height height of window
-     */
-    public TWindow(final TApplication application, final String title,
-        final int x, final int y, final int width, final int height) {
-
-        this(application, title, x, y, width, height, RESIZABLE);
+        this(application, title, x, y, width, height, RESIZABLE);
     }
 
     /**
@@ -545,310 +273,96 @@ public class TWindow extends TWidget {
         center();
 
         // Add me to the application
-        application.addWindow(this);
+        application.addWindowToApplication(this);
     }
 
     // ------------------------------------------------------------------------
-    // General behavior -------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
     // ------------------------------------------------------------------------
 
     /**
-     * See if this window is undergoing any movement/resize/etc.
+     * Returns true if the mouse is currently on the close button.
      *
-     * @return true if the window is moving
+     * @return true if mouse is currently on the close button
      */
-    public boolean inMovements() {
-        if (inWindowResize || inWindowMove || inKeyboardResize) {
+    protected boolean mouseOnClose() {
+        if ((flags & NOCLOSEBOX) != 0) {
+            return false;
+        }
+        if ((mouse != null)
+            && (mouse.getAbsoluteY() == getY())
+            && (mouse.getAbsoluteX() == getX() + 3)
+        ) {
             return true;
         }
         return false;
     }
 
     /**
-     * Stop any pending movement/resize/etc.
-     */
-    public void stopMovements() {
-        inWindowResize = false;
-        inWindowMove = false;
-        inKeyboardResize = false;
-    }
-
-    /**
-     * Returns true if this window is modal.
+     * Returns true if the mouse is currently on the maximize/restore button.
      *
-     * @return true if this window is modal
+     * @return true if the mouse is currently on the maximize/restore button
      */
-    public final boolean isModal() {
-        if ((flags & MODAL) == 0) {
+    protected boolean mouseOnMaximize() {
+        if ((flags & NOZOOMBOX) != 0) {
             return false;
         }
-        return true;
+        if ((mouse != null)
+            && !isModal()
+            && (mouse.getAbsoluteY() == getY())
+            && (mouse.getAbsoluteX() == getX() + getWidth() - 4)
+        ) {
+            return true;
+        }
+        return false;
     }
 
     /**
-     * Returns true if this window has a close box.
+     * Returns true if the mouse is currently on the resizable lower right
+     * corner.
      *
-     * @return true if this window has a close box
+     * @return true if the mouse is currently on the resizable lower right
+     * corner
      */
-    public final boolean hasCloseBox() {
-        if ((flags & NOCLOSEBOX) != 0) {
+    protected boolean mouseOnResize() {
+        if (((flags & RESIZABLE) != 0)
+            && !isModal()
+            && (mouse != null)
+            && (mouse.getAbsoluteY() == getY() + getHeight() - 1)
+            && ((mouse.getAbsoluteX() == getX() + getWidth() - 1)
+                || (mouse.getAbsoluteX() == getX() + getWidth() - 2))
+        ) {
             return true;
         }
         return false;
     }
 
     /**
-     * Returns true if this window has a maximize/zoom box.
-     *
-     * @return true if this window has a maximize/zoom box
+     * Subclasses should override this method to cleanup resources.  This is
+     * called by application.closeWindow().
      */
-    public final boolean hasZoomBox() {
-        if ((flags & NOZOOMBOX) != 0) {
-            return true;
-        }
-        return false;
+    public void onClose() {
+        // Default: do nothing
     }
 
     /**
-     * Retrieve the background color.
-     *
-     * @return the background color
+     * Called by application.switchWindow() when this window gets the
+     * focus, and also by application.addWindow().
      */
-    public CellAttributes getBackground() {
-        if (!isModal()
-            && (inWindowMove || inWindowResize || inKeyboardResize)
-        ) {
-            assert (isActive());
-            return getTheme().getColor("twindow.background.windowmove");
-        } else if (isModal() && inWindowMove) {
-            assert (isActive());
-            return getTheme().getColor("twindow.background.modal");
-        } else if (isModal()) {
-            if (isActive()) {
-                return getTheme().getColor("twindow.background.modal");
-            }
-            return getTheme().getColor("twindow.background.modal.inactive");
-        } else if (isActive()) {
-            assert (!isModal());
-            return getTheme().getColor("twindow.background");
-        } else {
-            assert (!isModal());
-            return getTheme().getColor("twindow.background.inactive");
-        }
+    public void onFocus() {
+        // Default: do nothing
     }
 
     /**
-     * Retrieve the border color.
-     *
-     * @return the border color
+     * Called by application.switchWindow() when another window gets the
+     * focus.
      */
-    public CellAttributes getBorder() {
-        if (!isModal()
-            && (inWindowMove || inWindowResize || inKeyboardResize)
-        ) {
-            assert (isActive());
-            return getTheme().getColor("twindow.border.windowmove");
-        } else if (isModal() && inWindowMove) {
-            assert (isActive());
-            return getTheme().getColor("twindow.border.modal.windowmove");
-        } else if (isModal()) {
-            if (isActive()) {
-                return getTheme().getColor("twindow.border.modal");
-            } else {
-                return getTheme().getColor("twindow.border.modal.inactive");
-            }
-        } else if (isActive()) {
-            assert (!isModal());
-            return getTheme().getColor("twindow.border");
-        } else {
-            assert (!isModal());
-            return getTheme().getColor("twindow.border.inactive");
-        }
+    public void onUnfocus() {
+        // Default: do nothing
     }
 
     /**
-     * Retrieve the border line type.
-     *
-     * @return the border line type
-     */
-    private int getBorderType() {
-        if (!isModal()
-            && (inWindowMove || inWindowResize || inKeyboardResize)
-        ) {
-            assert (isActive());
-            return 1;
-        } else if (isModal() && inWindowMove) {
-            assert (isActive());
-            return 1;
-        } else if (isModal()) {
-            if (isActive()) {
-                return 2;
-            } else {
-                return 1;
-            }
-        } else if (isActive()) {
-            return 2;
-        } else {
-            return 1;
-        }
-    }
-
-    /**
-     * Called by TApplication.drawChildren() to render on screen.
-     */
-    @Override
-    public void draw() {
-        // Draw the box and background first.
-        CellAttributes border = getBorder();
-        CellAttributes background = getBackground();
-        int borderType = getBorderType();
-
-        getScreen().drawBox(0, 0, getWidth(), getHeight(), border,
-            background, borderType, true);
-
-        // Draw the title
-        int titleLeft = (getWidth() - title.length() - 2) / 2;
-        putCharXY(titleLeft, 0, ' ', border);
-        putStringXY(titleLeft + 1, 0, title);
-        putCharXY(titleLeft + title.length() + 1, 0, ' ', border);
-
-        if (isActive()) {
-
-            // Draw the close button
-            if ((flags & NOCLOSEBOX) == 0) {
-                putCharXY(2, 0, '[', border);
-                putCharXY(4, 0, ']', border);
-                if (mouseOnClose() && mouse.isMouse1()) {
-                    putCharXY(3, 0, GraphicsChars.CP437[0x0F],
-                        !isModal()
-                        ? getTheme().getColor("twindow.border.windowmove")
-                        : getTheme().getColor("twindow.border.modal.windowmove"));
-                } else {
-                    putCharXY(3, 0, GraphicsChars.CP437[0xFE],
-                        !isModal()
-                        ? getTheme().getColor("twindow.border.windowmove")
-                        : getTheme().getColor("twindow.border.modal.windowmove"));
-                }
-            }
-
-            // Draw the maximize button
-            if (!isModal() && ((flags & NOZOOMBOX) == 0)) {
-
-                putCharXY(getWidth() - 5, 0, '[', border);
-                putCharXY(getWidth() - 3, 0, ']', border);
-                if (mouseOnMaximize() && mouse.isMouse1()) {
-                    putCharXY(getWidth() - 4, 0, GraphicsChars.CP437[0x0F],
-                        getTheme().getColor("twindow.border.windowmove"));
-                } else {
-                    if (maximized) {
-                        putCharXY(getWidth() - 4, 0, GraphicsChars.CP437[0x12],
-                            getTheme().getColor("twindow.border.windowmove"));
-                    } else {
-                        putCharXY(getWidth() - 4, 0, GraphicsChars.UPARROW,
-                            getTheme().getColor("twindow.border.windowmove"));
-                    }
-                }
-
-                // Draw the resize corner
-                if ((flags & RESIZABLE) != 0) {
-                    putCharXY(getWidth() - 2, getHeight() - 1,
-                        GraphicsChars.SINGLE_BAR,
-                        getTheme().getColor("twindow.border.windowmove"));
-                    putCharXY(getWidth() - 1, getHeight() - 1,
-                        GraphicsChars.LRCORNER,
-                        getTheme().getColor("twindow.border.windowmove"));
-                }
-            }
-        }
-    }
-
-    // ------------------------------------------------------------------------
-    // Event handlers ---------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Returns true if the mouse is currently on the close button.
-     *
-     * @return true if mouse is currently on the close button
-     */
-    protected boolean mouseOnClose() {
-        if ((flags & NOCLOSEBOX) != 0) {
-            return false;
-        }
-        if ((mouse != null)
-            && (mouse.getAbsoluteY() == getY())
-            && (mouse.getAbsoluteX() == getX() + 3)
-        ) {
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Returns true if the mouse is currently on the maximize/restore button.
-     *
-     * @return true if the mouse is currently on the maximize/restore button
-     */
-    protected boolean mouseOnMaximize() {
-        if ((flags & NOZOOMBOX) != 0) {
-            return false;
-        }
-        if ((mouse != null)
-            && !isModal()
-            && (mouse.getAbsoluteY() == getY())
-            && (mouse.getAbsoluteX() == getX() + getWidth() - 4)
-        ) {
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Returns true if the mouse is currently on the resizable lower right
-     * corner.
-     *
-     * @return true if the mouse is currently on the resizable lower right
-     * corner
-     */
-    protected boolean mouseOnResize() {
-        if (((flags & RESIZABLE) != 0)
-            && !isModal()
-            && (mouse != null)
-            && (mouse.getAbsoluteY() == getY() + getHeight() - 1)
-            && ((mouse.getAbsoluteX() == getX() + getWidth() - 1)
-                || (mouse.getAbsoluteX() == getX() + getWidth() - 2))
-        ) {
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Subclasses should override this method to cleanup resources.  This is
-     * called by application.closeWindow().
-     */
-    public void onClose() {
-        // Default: do nothing
-    }
-
-    /**
-     * Called by application.switchWindow() when this window gets the
-     * focus, and also by application.addWindow().
-     */
-    public void onFocus() {
-        // Default: do nothing
-    }
-
-    /**
-     * Called by application.switchWindow() when another window gets the
-     * focus.
-     */
-    public void onUnfocus() {
-        // Default: do nothing
-    }
-
-    /**
-     * Called by application.hideWindow().
+     * Called by application.hideWindow().
      */
     public void onHide() {
         // Default: do nothing
@@ -936,8 +450,13 @@ public class TWindow extends TWidget {
         }
 
         if (mouse.isMouse1() && mouseOnClose()) {
-            // Close window
-            application.closeWindow(this);
+            if ((flags & HIDEONCLOSE) == 0) {
+                // Close window
+                application.closeWindow(this);
+            } else {
+                // Hide window
+                application.hideWindow(this);
+            }
             return;
         }
 
@@ -1147,7 +666,13 @@ public class TWindow extends TWidget {
             // Ctrl-W - close window
             if (keypress.equals(kbCtrlW)) {
                 if ((flags & NOCLOSEBOX) == 0) {
-                    application.closeWindow(this);
+                    if ((flags & HIDEONCLOSE) == 0) {
+                        // Close window
+                        application.closeWindow(this);
+                    } else {
+                        // Hide window
+                        application.hideWindow(this);
+                    }
                 }
                 return;
             }
@@ -1200,7 +725,13 @@ public class TWindow extends TWidget {
 
             if (command.equals(cmWindowClose)) {
                 if ((flags & NOCLOSEBOX) == 0) {
-                    application.closeWindow(this);
+                    if ((flags & HIDEONCLOSE) == 0) {
+                        // Close window
+                        application.closeWindow(this);
+                    } else {
+                        // Hide window
+                        application.hideWindow(this);
+                    }
                 }
                 return;
             }
@@ -1246,7 +777,13 @@ public class TWindow extends TWidget {
 
             if (menu.getId() == TMenu.MID_WINDOW_CLOSE) {
                 if ((flags & NOCLOSEBOX) == 0) {
-                    application.closeWindow(this);
+                    if ((flags & HIDEONCLOSE) == 0) {
+                        // Close window
+                        application.closeWindow(this);
+                    } else {
+                        // Hide window
+                        application.hideWindow(this);
+                    }
                 }
                 return;
             }
@@ -1283,6 +820,509 @@ public class TWindow extends TWidget {
         super.onMenu(menu);
     }
 
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get this TWindow's parent TApplication.
+     *
+     * @return this TWindow's parent TApplication
+     */
+    @Override
+    public final TApplication getApplication() {
+        return application;
+    }
+
+    /**
+     * Get the Screen.
+     *
+     * @return the Screen
+     */
+    @Override
+    public final Screen getScreen() {
+        return application.getScreen();
+    }
+
+    /**
+     * Called by TApplication.drawChildren() to render on screen.
+     */
+    @Override
+    public void draw() {
+        // Draw the box and background first.
+        CellAttributes border = getBorder();
+        CellAttributes background = getBackground();
+        int borderType = getBorderType();
+
+        getScreen().drawBox(0, 0, getWidth(), getHeight(), border,
+            background, borderType, true);
+
+        // Draw the title
+        int titleLeft = (getWidth() - title.length() - 2) / 2;
+        putCharXY(titleLeft, 0, ' ', border);
+        putStringXY(titleLeft + 1, 0, title);
+        putCharXY(titleLeft + title.length() + 1, 0, ' ', border);
+
+        if (isActive()) {
+
+            // Draw the close button
+            if ((flags & NOCLOSEBOX) == 0) {
+                putCharXY(2, 0, '[', border);
+                putCharXY(4, 0, ']', border);
+                if (mouseOnClose() && mouse.isMouse1()) {
+                    putCharXY(3, 0, GraphicsChars.CP437[0x0F],
+                        getBorderControls());
+                } else {
+                    putCharXY(3, 0, GraphicsChars.CP437[0xFE],
+                        getBorderControls());
+                }
+            }
+
+            // Draw the maximize button
+            if (!isModal() && ((flags & NOZOOMBOX) == 0)) {
+
+                putCharXY(getWidth() - 5, 0, '[', border);
+                putCharXY(getWidth() - 3, 0, ']', border);
+                if (mouseOnMaximize() && mouse.isMouse1()) {
+                    putCharXY(getWidth() - 4, 0, GraphicsChars.CP437[0x0F],
+                        getBorderControls());
+                } else {
+                    if (maximized) {
+                        putCharXY(getWidth() - 4, 0, GraphicsChars.CP437[0x12],
+                            getBorderControls());
+                    } else {
+                        putCharXY(getWidth() - 4, 0, GraphicsChars.UPARROW,
+                            getBorderControls());
+                    }
+                }
+
+                // Draw the resize corner
+                if ((flags & RESIZABLE) != 0) {
+                    putCharXY(getWidth() - 2, getHeight() - 1,
+                        GraphicsChars.SINGLE_BAR, getBorderControls());
+                    putCharXY(getWidth() - 1, getHeight() - 1,
+                        GraphicsChars.LRCORNER, getBorderControls());
+                }
+            }
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // TWindow ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get window title.
+     *
+     * @return window title
+     */
+    public final String getTitle() {
+        return title;
+    }
+
+    /**
+     * Set window title.
+     *
+     * @param title new window title
+     */
+    public final void setTitle(final String title) {
+        this.title = title;
+    }
+
+    /**
+     * Get Z order.  Lower number means more in-front.
+     *
+     * @return Z value.  Lower number means more in-front.
+     */
+    public final int getZ() {
+        return z;
+    }
+
+    /**
+     * Set Z order.  Lower number means more in-front.
+     *
+     * @param z the new Z value.  Lower number means more in-front.
+     */
+    public final void setZ(final int z) {
+        this.z = z;
+    }
+
+    /**
+     * Add a keypress to be overridden for this window.
+     *
+     * @param key the key to start taking control of
+     */
+    protected void addShortcutKeypress(final TKeypress key) {
+        keyboardShortcuts.add(key);
+    }
+
+    /**
+     * Remove a keypress to be overridden for this window.
+     *
+     * @param key the key to stop taking control of
+     */
+    protected void removeShortcutKeypress(final TKeypress key) {
+        keyboardShortcuts.remove(key);
+    }
+
+    /**
+     * Remove all keypresses to be overridden for this window.
+     */
+    protected void clearShortcutKeypresses() {
+        keyboardShortcuts.clear();
+    }
+
+    /**
+     * Determine if a keypress is overridden for this window.
+     *
+     * @param key the key to check
+     * @return true if this window wants to process this key on its own
+     */
+    public boolean isShortcutKeypress(final TKeypress key) {
+        return keyboardShortcuts.contains(key);
+    }
+
+    /**
+     * Get the window's status bar, or null if it does not have one.
+     *
+     * @return the status bar, or null
+     */
+    public TStatusBar getStatusBar() {
+        return statusBar;
+    }
+
+    /**
+     * Set the window's status bar to a new one.
+     *
+     * @param text the status bar text
+     * @return the status bar
+     */
+    public TStatusBar newStatusBar(final String text) {
+        statusBar = new TStatusBar(this, text);
+        return statusBar;
+    }
+
+    /**
+     * Set the maximum width for this window.
+     *
+     * @param maximumWindowWidth new maximum width
+     */
+    public final void setMaximumWindowWidth(final int maximumWindowWidth) {
+        if ((maximumWindowWidth != -1)
+            && (maximumWindowWidth < minimumWindowWidth + 1)
+        ) {
+            throw new IllegalArgumentException("Maximum window width cannot " +
+                "be smaller than minimum window width + 1");
+        }
+        this.maximumWindowWidth = maximumWindowWidth;
+    }
+
+    /**
+     * Set the minimum width for this window.
+     *
+     * @param minimumWindowWidth new minimum width
+     */
+    public final void setMinimumWindowWidth(final int minimumWindowWidth) {
+        if ((maximumWindowWidth != -1)
+            && (minimumWindowWidth > maximumWindowWidth - 1)
+        ) {
+            throw new IllegalArgumentException("Minimum window width cannot " +
+                "be larger than maximum window width - 1");
+        }
+        this.minimumWindowWidth = minimumWindowWidth;
+    }
+
+    /**
+     * Set the maximum height for this window.
+     *
+     * @param maximumWindowHeight new maximum height
+     */
+    public final void setMaximumWindowHeight(final int maximumWindowHeight) {
+        if ((maximumWindowHeight != -1)
+            && (maximumWindowHeight < minimumWindowHeight + 1)
+        ) {
+            throw new IllegalArgumentException("Maximum window height cannot " +
+                "be smaller than minimum window height + 1");
+        }
+        this.maximumWindowHeight = maximumWindowHeight;
+    }
+
+    /**
+     * Set the minimum height for this window.
+     *
+     * @param minimumWindowHeight new minimum height
+     */
+    public final void setMinimumWindowHeight(final int minimumWindowHeight) {
+        if ((maximumWindowHeight != -1)
+            && (minimumWindowHeight > maximumWindowHeight - 1)
+        ) {
+            throw new IllegalArgumentException("Minimum window height cannot " +
+                "be larger than maximum window height - 1");
+        }
+        this.minimumWindowHeight = minimumWindowHeight;
+    }
+
+    /**
+     * Recenter the window on-screen.
+     */
+    public final void center() {
+        if ((flags & CENTERED) != 0) {
+            if (getWidth() < getScreen().getWidth()) {
+                setX((getScreen().getWidth() - getWidth()) / 2);
+            } else {
+                setX(0);
+            }
+            setY(((application.getDesktopBottom()
+                    - application.getDesktopTop()) - getHeight()) / 2);
+            if (getY() < 0) {
+                setY(0);
+            }
+            setY(getY() + application.getDesktopTop());
+        }
+    }
+
+    /**
+     * Maximize window.
+     */
+    public void maximize() {
+        if (maximized) {
+            return;
+        }
+
+        restoreWindowWidth = getWidth();
+        restoreWindowHeight = getHeight();
+        restoreWindowX = getX();
+        restoreWindowY = getY();
+        setWidth(getScreen().getWidth());
+        setHeight(application.getDesktopBottom() - 1);
+        setX(0);
+        setY(1);
+        maximized = true;
+
+        onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
+                getHeight()));
+    }
+
+    /**
+     * Restore (unmaximize) window.
+     */
+    public void restore() {
+        if (!maximized) {
+            return;
+        }
+
+        setWidth(restoreWindowWidth);
+        setHeight(restoreWindowHeight);
+        setX(restoreWindowX);
+        setY(restoreWindowY);
+        maximized = false;
+
+        onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
+                getHeight()));
+    }
+
+    /**
+     * Returns true if this window is hidden.
+     *
+     * @return true if this window is hidden, false if the window is shown
+     */
+    public final boolean isHidden() {
+        return hidden;
+    }
+
+    /**
+     * Returns true if this window is shown.
+     *
+     * @return true if this window is shown, false if the window is hidden
+     */
+    public final boolean isShown() {
+        return !hidden;
+    }
+
+    /**
+     * Hide window.  A hidden window will still have its onIdle() called, and
+     * will also have onClose() called at application exit.  Hidden windows
+     * will not receive any other events.
+     */
+    public void hide() {
+        application.hideWindow(this);
+    }
+
+    /**
+     * Show window.
+     */
+    public void show() {
+        application.showWindow(this);
+    }
+
+    /**
+     * Activate window (bring to top and receive events).
+     */
+    public void activate() {
+        application.activateWindow(this);
+    }
+
+    /**
+     * Close window.  Note that windows without a close box can still be
+     * closed by calling the close() method.
+     */
+    public void close() {
+        application.closeWindow(this);
+    }
+
+    /**
+     * See if this window is undergoing any movement/resize/etc.
+     *
+     * @return true if the window is moving
+     */
+    public boolean inMovements() {
+        if (inWindowResize || inWindowMove || inKeyboardResize) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Stop any pending movement/resize/etc.
+     */
+    public void stopMovements() {
+        inWindowResize = false;
+        inWindowMove = false;
+        inKeyboardResize = false;
+    }
+
+    /**
+     * Returns true if this window is modal.
+     *
+     * @return true if this window is modal
+     */
+    public final boolean isModal() {
+        if ((flags & MODAL) == 0) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Returns true if this window has a close box.
+     *
+     * @return true if this window has a close box
+     */
+    public final boolean hasCloseBox() {
+        if ((flags & NOCLOSEBOX) != 0) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if this window has a maximize/zoom box.
+     *
+     * @return true if this window has a maximize/zoom box
+     */
+    public final boolean hasZoomBox() {
+        if ((flags & NOZOOMBOX) != 0) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Retrieve the background color.
+     *
+     * @return the background color
+     */
+    public CellAttributes getBackground() {
+        if (!isModal()
+            && (inWindowMove || inWindowResize || inKeyboardResize)
+        ) {
+            assert (isActive());
+            return getTheme().getColor("twindow.background.windowmove");
+        } else if (isModal() && inWindowMove) {
+            assert (isActive());
+            return getTheme().getColor("twindow.background.modal");
+        } else if (isModal()) {
+            if (isActive()) {
+                return getTheme().getColor("twindow.background.modal");
+            }
+            return getTheme().getColor("twindow.background.modal.inactive");
+        } else if (isActive()) {
+            assert (!isModal());
+            return getTheme().getColor("twindow.background");
+        } else {
+            assert (!isModal());
+            return getTheme().getColor("twindow.background.inactive");
+        }
+    }
+
+    /**
+     * Retrieve the border color.
+     *
+     * @return the border color
+     */
+    public CellAttributes getBorder() {
+        if (!isModal()
+            && (inWindowMove || inWindowResize || inKeyboardResize)
+        ) {
+            assert (isActive());
+            return getTheme().getColor("twindow.border.windowmove");
+        } else if (isModal() && inWindowMove) {
+            assert (isActive());
+            return getTheme().getColor("twindow.border.modal.windowmove");
+        } else if (isModal()) {
+            if (isActive()) {
+                return getTheme().getColor("twindow.border.modal");
+            } else {
+                return getTheme().getColor("twindow.border.modal.inactive");
+            }
+        } else if (isActive()) {
+            assert (!isModal());
+            return getTheme().getColor("twindow.border");
+        } else {
+            assert (!isModal());
+            return getTheme().getColor("twindow.border.inactive");
+        }
+    }
+
+    /**
+     * Retrieve the color used by the window movement/sizing controls.
+     *
+     * @return the color used by the zoom box, resize bar, and close box
+     */
+    public CellAttributes getBorderControls() {
+        if (isModal()) {
+            return getTheme().getColor("twindow.border.modal.windowmove");
+        }
+        return getTheme().getColor("twindow.border.windowmove");
+    }
+
+    /**
+     * Retrieve the border line type.
+     *
+     * @return the border line type
+     */
+    private int getBorderType() {
+        if (!isModal()
+            && (inWindowMove || inWindowResize || inKeyboardResize)
+        ) {
+            assert (isActive());
+            return 1;
+        } else if (isModal() && inWindowMove) {
+            assert (isActive());
+            return 1;
+        } else if (isModal()) {
+            if (isActive()) {
+                return 2;
+            } else {
+                return 1;
+            }
+        } else if (isActive()) {
+            return 2;
+        } else {
+            return 1;
+        }
+    }
+
     // ------------------------------------------------------------------------
     // Passthru for Screen functions ------------------------------------------
     // ------------------------------------------------------------------------
index d1863288c54dabd8dfe4f0bb5b59f9356397db8b..20efa7eeaec8ad913387a4a546dcab512f41d512 100644 (file)
@@ -60,6 +60,13 @@ public interface Backend {
      */
     public void flushScreen();
 
+    /**
+     * Check if there are events in the queue.
+     *
+     * @return if true, getEvents() has something to return to the application
+     */
+    public boolean hasEvents();
+
     /**
      * Classes must provide an implementation to get keyboard, mouse, and
      * screen resize events.
index 59cd670552b482b201fca443c966763988ab086c..b99c12014edc85506b6a867534159387bbab4bca 100644 (file)
@@ -40,6 +40,10 @@ import java.io.UnsupportedEncodingException;
  */
 public final class ECMA48Backend extends GenericBackend {
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Public constructor will use System.in and System.out and UTF-8
      * encoding. On non-Windows systems System.in will be put in raw mode;
@@ -161,5 +165,4 @@ public final class ECMA48Backend extends GenericBackend {
         this(listener, input, reader, writer, false);
     }
 
-
 }
index ae356107ee9c05f3d049f8bbb8b969a1aea50135..360994a1841bea9bdf820d0d74bf9f5b5e7d631c 100644 (file)
@@ -59,6 +59,27 @@ import static jexer.TKeypress.*;
 public final class ECMA48Terminal extends LogicalScreen
                                   implements TerminalReader, Runnable {
 
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * States in the input parser.
+     */
+    private enum ParseState {
+        GROUND,
+        ESCAPE,
+        ESCAPE_INTERMEDIATE,
+        CSI_ENTRY,
+        CSI_PARAM,
+        MOUSE,
+        MOUSE_SGR,
+    }
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Emit debugging to stderr.
      */
@@ -75,15 +96,6 @@ public final class ECMA48Terminal extends LogicalScreen
      */
     private SessionInfo sessionInfo;
 
-    /**
-     * Getter for sessionInfo.
-     *
-     * @return the SessionInfo
-     */
-    public SessionInfo getSessionInfo() {
-        return sessionInfo;
-    }
-
     /**
      * The event queue, filled up by a thread reading on input.
      */
@@ -103,20 +115,7 @@ public final class ECMA48Terminal extends LogicalScreen
      * Parameters being collected.  E.g. if the string is \033[1;3m, then
      * params[0] will be 1 and params[1] will be 3.
      */
-    private ArrayList<String> params;
-
-    /**
-     * States in the input parser.
-     */
-    private enum ParseState {
-        GROUND,
-        ESCAPE,
-        ESCAPE_INTERMEDIATE,
-        CSI_ENTRY,
-        CSI_PARAM,
-        MOUSE,
-        MOUSE_SGR,
-    }
+    private List<String> params;
 
     /**
      * Current parsing state.
@@ -194,101 +193,9 @@ public final class ECMA48Terminal extends LogicalScreen
      */
     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;
-    }
-
-    /**
-     * Get the output writer.
-     *
-     * @return the Writer
-     */
-    public PrintWriter getOutput() {
-        return output;
-    }
-
-    /**
-     * 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);
-        }
-    }
-
-    /**
-     * Call 'stty' to set cooked mode.
-     *
-     * <p>Actually executes '/bin/sh -c stty sane cooked &lt; /dev/tty'
-     */
-    private void sttyCooked() {
-        doStty(false);
-    }
-
-    /**
-     * Call 'stty' to set raw mode.
-     *
-     * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
-     * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
-     * -parenb cs8 min 1 &lt; /dev/tty'
-     */
-    private void sttyRaw() {
-        doStty(true);
-    }
-
-    /**
-     * Call 'stty' to set raw or cooked mode.
-     *
-     * @param mode if true, set raw mode, otherwise set cooked mode
-     */
-    private void doStty(final boolean mode) {
-        String [] cmdRaw = {
-            "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
-        };
-        String [] cmdCooked = {
-            "/bin/sh", "-c", "stty sane cooked < /dev/tty"
-        };
-        try {
-            Process process;
-            if (mode) {
-                process = Runtime.getRuntime().exec(cmdRaw);
-            } else {
-                process = Runtime.getRuntime().exec(cmdCooked);
-            }
-            BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
-            String line = in.readLine();
-            if ((line != null) && (line.length() > 0)) {
-                System.err.println("WEIRD?! Normal output from stty: " + line);
-            }
-            while (true) {
-                BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
-                line = err.readLine();
-                if ((line != null) && (line.length() > 0)) {
-                    System.err.println("Error output from stty: " + line);
-                }
-                try {
-                    process.waitFor();
-                    break;
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-            }
-            int rc = process.exitValue();
-            if (rc != 0) {
-                System.err.println("stty returned error code: " + rc);
-            }
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
-    }
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Constructor sets up state for getEvent().
@@ -517,6 +424,73 @@ public final class ECMA48Terminal extends LogicalScreen
         this(listener, input, reader, writer, false);
     }
 
+    // ------------------------------------------------------------------------
+    // LogicalScreen ----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Set the window title.
+     *
+     * @param title the new title
+     */
+    @Override
+    public void setTitle(final String title) {
+        output.write(getSetTitleString(title));
+        flush();
+    }
+
+    /**
+     * Push the logical screen to the physical device.
+     */
+    @Override
+    public void flushPhysical() {
+        String result = flushString();
+        if ((cursorVisible)
+            && (cursorY >= 0)
+            && (cursorX >= 0)
+            && (cursorY <= height - 1)
+            && (cursorX <= width - 1)
+        ) {
+            result += cursor(true);
+            result += gotoXY(cursorX, cursorY);
+        } else {
+            result += cursor(false);
+        }
+        output.write(result);
+        flush();
+    }
+
+    // ------------------------------------------------------------------------
+    // TerminalReader ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * 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<TInputEvent> queue) {
+        synchronized (eventQueue) {
+            if (eventQueue.size() > 0) {
+                synchronized (queue) {
+                    queue.addAll(eventQueue);
+                }
+                eventQueue.clear();
+            }
+        }
+    }
+
     /**
      * Restore terminal to normal state.
      */
@@ -558,6 +532,195 @@ public final class ECMA48Terminal extends LogicalScreen
         }
     }
 
+    /**
+     * 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;
+    }
+
+    // ------------------------------------------------------------------------
+    // Runnable ---------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Read function runs on a separate thread.
+     */
+    public void run() {
+        boolean done = false;
+        // available() will often return > 1, so we need to read in chunks to
+        // stay caught up.
+        char [] readBuffer = new char[128];
+        List<TInputEvent> events = new LinkedList<TInputEvent>();
+
+        while (!done && !stopReaderThread) {
+            try {
+                // We assume that if inputStream has bytes available, then
+                // input won't block on read().
+                int n = inputStream.available();
+
+                /*
+                System.err.printf("inputStream.available(): %d\n", n);
+                System.err.flush();
+                */
+
+                if (n > 0) {
+                    if (readBuffer.length < n) {
+                        // The buffer wasn't big enough, make it huger
+                        readBuffer = new char[readBuffer.length * 2];
+                    }
+
+                    // System.err.printf("BEFORE read()\n"); System.err.flush();
+
+                    int rc = input.read(readBuffer, 0, readBuffer.length);
+
+                    /*
+                    System.err.printf("AFTER read() %d\n", rc);
+                    System.err.flush();
+                    */
+
+                    if (rc == -1) {
+                        // This is EOF
+                        done = true;
+                    } else {
+                        for (int i = 0; i < rc; i++) {
+                            int ch = readBuffer[i];
+                            processChar(events, (char)ch);
+                        }
+                        getIdleEvents(events);
+                        if (events.size() > 0) {
+                            // Add to the queue for the backend thread to
+                            // be able to obtain.
+                            synchronized (eventQueue) {
+                                eventQueue.addAll(events);
+                            }
+                            if (listener != null) {
+                                synchronized (listener) {
+                                    listener.notifyAll();
+                                }
+                            }
+                            events.clear();
+                        }
+                    }
+                } else {
+                    getIdleEvents(events);
+                    if (events.size() > 0) {
+                        synchronized (eventQueue) {
+                            eventQueue.addAll(events);
+                        }
+                        if (listener != null) {
+                            synchronized (listener) {
+                                listener.notifyAll();
+                            }
+                        }
+                        events.clear();
+                    }
+
+                    // Wait 20 millis for more data
+                    Thread.sleep(20);
+                }
+                // System.err.println("end while loop"); System.err.flush();
+            } catch (InterruptedException e) {
+                // SQUASH
+            } catch (IOException e) {
+                e.printStackTrace();
+                done = true;
+            }
+        } // while ((done == false) && (stopReaderThread == false))
+        // System.err.println("*** run() exiting..."); System.err.flush();
+    }
+
+    // ------------------------------------------------------------------------
+    // ECMA48Terminal ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Getter for sessionInfo.
+     *
+     * @return the SessionInfo
+     */
+    public SessionInfo getSessionInfo() {
+        return sessionInfo;
+    }
+
+    /**
+     * Get the output writer.
+     *
+     * @return the Writer
+     */
+    public PrintWriter getOutput() {
+        return output;
+    }
+
+    /**
+     * Call 'stty' to set cooked mode.
+     *
+     * <p>Actually executes '/bin/sh -c stty sane cooked &lt; /dev/tty'
+     */
+    private void sttyCooked() {
+        doStty(false);
+    }
+
+    /**
+     * Call 'stty' to set raw mode.
+     *
+     * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
+     * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
+     * -parenb cs8 min 1 &lt; /dev/tty'
+     */
+    private void sttyRaw() {
+        doStty(true);
+    }
+
+    /**
+     * Call 'stty' to set raw or cooked mode.
+     *
+     * @param mode if true, set raw mode, otherwise set cooked mode
+     */
+    private void doStty(final boolean mode) {
+        String [] cmdRaw = {
+            "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
+        };
+        String [] cmdCooked = {
+            "/bin/sh", "-c", "stty sane cooked < /dev/tty"
+        };
+        try {
+            Process process;
+            if (mode) {
+                process = Runtime.getRuntime().exec(cmdRaw);
+            } else {
+                process = Runtime.getRuntime().exec(cmdCooked);
+            }
+            BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
+            String line = in.readLine();
+            if ((line != null) && (line.length() > 0)) {
+                System.err.println("WEIRD?! Normal output from stty: " + line);
+            }
+            while (true) {
+                BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
+                line = err.readLine();
+                if ((line != null) && (line.length() > 0)) {
+                    System.err.println("Error output from stty: " + line);
+                }
+                try {
+                    process.waitFor();
+                    break;
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }
+            int rc = process.exitValue();
+            if (rc != 0) {
+                System.err.println("stty returned error code: " + rc);
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
     /**
      * Flush output.
      */
@@ -763,37 +926,6 @@ public final class ECMA48Terminal extends LogicalScreen
         return result;
     }
 
-    /**
-     * Push the logical screen to the physical device.
-     */
-    @Override
-    public void flushPhysical() {
-        String result = flushString();
-        if ((cursorVisible)
-            && (cursorY >= 0)
-            && (cursorX >= 0)
-            && (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.
      */
@@ -1108,22 +1240,6 @@ public final class ECMA48Terminal extends LogicalScreen
             eventMouseWheelUp, eventMouseWheelDown);
     }
 
-    /**
-     * Return any events in the IO queue.
-     *
-     * @param queue list to append new events to
-     */
-    public void getEvents(final List<TInputEvent> queue) {
-        synchronized (eventQueue) {
-            if (eventQueue.size() > 0) {
-                synchronized (queue) {
-                    queue.addAll(eventQueue);
-                }
-                eventQueue.clear();
-            }
-        }
-    }
-
     /**
      * Return any events in the IO queue due to timeout.
      *
@@ -1895,91 +2011,4 @@ public final class ECMA48Terminal extends LogicalScreen
         return "\033[?1002;1003;1006;1005l\033[?1049l";
     }
 
-    /**
-     * Read function runs on a separate thread.
-     */
-    public void run() {
-        boolean done = false;
-        // available() will often return > 1, so we need to read in chunks to
-        // stay caught up.
-        char [] readBuffer = new char[128];
-        List<TInputEvent> events = new LinkedList<TInputEvent>();
-
-        while (!done && !stopReaderThread) {
-            try {
-                // We assume that if inputStream has bytes available, then
-                // input won't block on read().
-                int n = inputStream.available();
-
-                /*
-                System.err.printf("inputStream.available(): %d\n", n);
-                System.err.flush();
-                */
-
-                if (n > 0) {
-                    if (readBuffer.length < n) {
-                        // The buffer wasn't big enough, make it huger
-                        readBuffer = new char[readBuffer.length * 2];
-                    }
-
-                    // System.err.printf("BEFORE read()\n"); System.err.flush();
-
-                    int rc = input.read(readBuffer, 0, readBuffer.length);
-
-                    /*
-                    System.err.printf("AFTER read() %d\n", rc);
-                    System.err.flush();
-                    */
-
-                    if (rc == -1) {
-                        // This is EOF
-                        done = true;
-                    } else {
-                        for (int i = 0; i < rc; i++) {
-                            int ch = readBuffer[i];
-                            processChar(events, (char)ch);
-                        }
-                        getIdleEvents(events);
-                        if (events.size() > 0) {
-                            // Add to the queue for the backend thread to
-                            // be able to obtain.
-                            synchronized (eventQueue) {
-                                eventQueue.addAll(events);
-                            }
-                            if (listener != null) {
-                                synchronized (listener) {
-                                    listener.notifyAll();
-                                }
-                            }
-                            events.clear();
-                        }
-                    }
-                } else {
-                    getIdleEvents(events);
-                    if (events.size() > 0) {
-                        synchronized (eventQueue) {
-                            eventQueue.addAll(events);
-                        }
-                        if (listener != null) {
-                            synchronized (listener) {
-                                listener.notifyAll();
-                            }
-                        }
-                        events.clear();
-                    }
-
-                    // Wait 20 millis for more data
-                    Thread.sleep(20);
-                }
-                // System.err.println("end while loop"); System.err.flush();
-            } catch (InterruptedException e) {
-                // SQUASH
-            } catch (IOException e) {
-                e.printStackTrace();
-                done = true;
-            }
-        } // while ((done == false) && (stopReaderThread == false))
-        // System.err.println("*** run() exiting..."); System.err.flush();
-    }
-
 }
index bf27e947607f1c7bea073b4edac0689f300e6067..bb9ae9f95af83a52c618541f121df6cbb0faecc5 100644 (file)
@@ -39,11 +39,33 @@ import jexer.event.TInputEvent;
  */
 public abstract class GenericBackend implements Backend {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The session information.
      */
     protected SessionInfo sessionInfo;
 
+    /**
+     * The screen to draw on.
+     */
+    protected Screen screen;
+
+    /**
+     * Input events are processed by this Terminal.
+     */
+    protected TerminalReader terminal;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    // ------------------------------------------------------------------------
+    // Backend ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Getter for sessionInfo.
      *
@@ -53,11 +75,6 @@ public abstract class GenericBackend implements Backend {
         return sessionInfo;
     }
 
-    /**
-     * The screen to draw on.
-     */
-    protected Screen screen;
-
     /**
      * Getter for screen.
      *
@@ -75,9 +92,13 @@ public abstract class GenericBackend implements Backend {
     }
 
     /**
-     * Input events are processed by this Terminal.
+     * Check if there are events in the queue.
+     *
+     * @return if true, getEvents() has something to return to the application
      */
-    protected TerminalReader terminal;
+    public boolean hasEvents() {
+        return terminal.hasEvents();
+    }
 
     /**
      * Get keyboard, mouse, and screen resize events.
index c9b025abb257cb260f1ecff0f62a97e792d4caef..c24703e7a23098740804f5be8f4873b27ceb1324 100644 (file)
@@ -37,6 +37,10 @@ import jexer.bits.GraphicsChars;
  */
 public class LogicalScreen implements Screen {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Width of the visible window.
      */
@@ -52,6 +56,84 @@ public class LogicalScreen implements Screen {
      */
     private int offsetX;
 
+    /**
+     * Drawing offset for y.
+     */
+    private int offsetY;
+
+    /**
+     * Ignore anything drawn right of clipRight.
+     */
+    private int clipRight;
+
+    /**
+     * Ignore anything drawn below clipBottom.
+     */
+    private int clipBottom;
+
+    /**
+     * Ignore anything drawn left of clipLeft.
+     */
+    private int clipLeft;
+
+    /**
+     * Ignore anything drawn above clipTop.
+     */
+    private int clipTop;
+
+    /**
+     * The physical screen last sent out on flush().
+     */
+    protected Cell [][] physical;
+
+    /**
+     * The logical screen being rendered to.
+     */
+    protected Cell [][] logical;
+
+    /**
+     * Set if the user explicitly wants to redraw everything starting with a
+     * ECMATerminal.clearAll().
+     */
+    protected boolean reallyCleared;
+
+    /**
+     * If true, the cursor is visible and should be placed onscreen at
+     * (cursorX, cursorY) during a call to flushPhysical().
+     */
+    protected boolean cursorVisible;
+
+    /**
+     * Cursor X position if visible.
+     */
+    protected int cursorX;
+
+    /**
+     * Cursor Y position if visible.
+     */
+    protected int cursorY;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.  Sets everything to not-bold, white-on-black.
+     */
+    protected LogicalScreen() {
+        offsetX  = 0;
+        offsetY  = 0;
+        width    = 80;
+        height   = 24;
+        logical  = null;
+        physical = null;
+        reallocate(width, height);
+    }
+
+    // ------------------------------------------------------------------------
+    // Screen -----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Set drawing offset for x.
      *
@@ -61,11 +143,6 @@ public class LogicalScreen implements Screen {
         this.offsetX = offsetX;
     }
 
-    /**
-     * Drawing offset for y.
-     */
-    private int offsetY;
-
     /**
      * Set drawing offset for y.
      *
@@ -75,11 +152,6 @@ public class LogicalScreen implements Screen {
         this.offsetY = offsetY;
     }
 
-    /**
-     * Ignore anything drawn right of clipRight.
-     */
-    private int clipRight;
-
     /**
      * Get right drawing clipping boundary.
      *
@@ -98,11 +170,6 @@ public class LogicalScreen implements Screen {
         this.clipRight = clipRight;
     }
 
-    /**
-     * Ignore anything drawn below clipBottom.
-     */
-    private int clipBottom;
-
     /**
      * Get bottom drawing clipping boundary.
      *
@@ -121,11 +188,6 @@ public class LogicalScreen implements Screen {
         this.clipBottom = clipBottom;
     }
 
-    /**
-     * Ignore anything drawn left of clipLeft.
-     */
-    private int clipLeft;
-
     /**
      * Get left drawing clipping boundary.
      *
@@ -144,11 +206,6 @@ public class LogicalScreen implements Screen {
         this.clipLeft = clipLeft;
     }
 
-    /**
-     * Ignore anything drawn above clipTop.
-     */
-    private int clipTop;
-
     /**
      * Get top drawing clipping boundary.
      *
@@ -167,16 +224,6 @@ public class LogicalScreen implements Screen {
         this.clipTop = clipTop;
     }
 
-    /**
-     * The physical screen last sent out on flush().
-     */
-    protected Cell [][] physical;
-
-    /**
-     * The logical screen being rendered to.
-     */
-    protected Cell [][] logical;
-
     /**
      * Get dirty flag.
      *
@@ -200,28 +247,6 @@ public class LogicalScreen implements Screen {
         return false;
     }
 
-    /**
-     * Set if the user explicitly wants to redraw everything starting with a
-     * ECMATerminal.clearAll().
-     */
-    protected boolean reallyCleared;
-
-    /**
-     * If true, the cursor is visible and should be placed onscreen at
-     * (cursorX, cursorY) during a call to flushPhysical().
-     */
-    protected boolean cursorVisible;
-
-    /**
-     * Cursor X position if visible.
-     */
-    protected int cursorX;
-
-    /**
-     * Cursor Y position if visible.
-     */
-    protected int cursorY;
-
     /**
      * Get the attributes at one location.
      *
@@ -491,50 +516,6 @@ public class LogicalScreen implements Screen {
         }
     }
 
-    /**
-     * Reallocate screen buffers.
-     *
-     * @param width new width
-     * @param height new height
-     */
-    private synchronized void reallocate(final int width, final int height) {
-        if (logical != null) {
-            for (int row = 0; row < this.height; row++) {
-                for (int col = 0; col < this.width; col++) {
-                    logical[col][row] = null;
-                }
-            }
-            logical = null;
-        }
-        logical = new Cell[width][height];
-        if (physical != null) {
-            for (int row = 0; row < this.height; row++) {
-                for (int col = 0; col < this.width; col++) {
-                    physical[col][row] = null;
-                }
-            }
-            physical = null;
-        }
-        physical = new Cell[width][height];
-
-        for (int row = 0; row < height; row++) {
-            for (int col = 0; col < width; col++) {
-                physical[col][row] = new Cell();
-                logical[col][row] = new Cell();
-            }
-        }
-
-        this.width = width;
-        this.height = height;
-
-        clipLeft = 0;
-        clipTop = 0;
-        clipRight = width;
-        clipBottom = height;
-
-        reallyCleared = true;
-    }
-
     /**
      * Change the width.  Everything on-screen will be destroyed and must be
      * redrawn.
@@ -584,19 +565,6 @@ public class LogicalScreen implements Screen {
         return this.width;
     }
 
-    /**
-     * Public constructor.  Sets everything to not-bold, white-on-black.
-     */
-    protected LogicalScreen() {
-        offsetX  = 0;
-        offsetY  = 0;
-        width    = 80;
-        height   = 24;
-        logical  = null;
-        physical = null;
-        reallocate(width, height);
-    }
-
     /**
      * Reset screen to not-bold, white-on-black.  Also flushes the offset and
      * clip variables.
@@ -629,17 +597,6 @@ public class LogicalScreen implements Screen {
         reset();
     }
 
-    /**
-     * Clear the physical screen.
-     */
-    public final void clearPhysical() {
-        for (int row = 0; row < height; row++) {
-            for (int col = 0; col < width; col++) {
-                physical[col][row].reset();
-            }
-        }
-    }
-
     /**
      * Draw a box with a border and empty background.
      *
@@ -852,4 +809,63 @@ public class LogicalScreen implements Screen {
      */
     public void setTitle(final String title) {}
 
+    // ------------------------------------------------------------------------
+    // LogicalScreen ----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Reallocate screen buffers.
+     *
+     * @param width new width
+     * @param height new height
+     */
+    private synchronized void reallocate(final int width, final int height) {
+        if (logical != null) {
+            for (int row = 0; row < this.height; row++) {
+                for (int col = 0; col < this.width; col++) {
+                    logical[col][row] = null;
+                }
+            }
+            logical = null;
+        }
+        logical = new Cell[width][height];
+        if (physical != null) {
+            for (int row = 0; row < this.height; row++) {
+                for (int col = 0; col < this.width; col++) {
+                    physical[col][row] = null;
+                }
+            }
+            physical = null;
+        }
+        physical = new Cell[width][height];
+
+        for (int row = 0; row < height; row++) {
+            for (int col = 0; col < width; col++) {
+                physical[col][row] = new Cell();
+                logical[col][row] = new Cell();
+            }
+        }
+
+        this.width = width;
+        this.height = height;
+
+        clipLeft = 0;
+        clipTop = 0;
+        clipRight = width;
+        clipBottom = height;
+
+        reallyCleared = true;
+    }
+
+    /**
+     * Clear the physical screen.
+     */
+    public final void clearPhysical() {
+        for (int row = 0; row < height; row++) {
+            for (int col = 0; col < width; col++) {
+                physical[col][row].reset();
+            }
+        }
+    }
+
 }
index 9166e1c19bec44817a21297d87d949cb27b1a1f2..4e82d4ee694e13cbe12275ca1e16f67b42391251 100644 (file)
@@ -38,6 +38,10 @@ import jexer.event.TInputEvent;
  */
 public class MultiBackend implements Backend {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The screen to use.
      */
@@ -48,6 +52,10 @@ public class MultiBackend implements Backend {
      */
     private List<Backend> backends = new LinkedList<Backend>();
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Public constructor requires one backend.  Note that this backend's
      * screen will be replaced with a MultiScreen.
@@ -63,35 +71,9 @@ public class MultiBackend implements Backend {
         }
     }
 
-    /**
-     * Add a backend to the list.
-     *
-     * @param backend the backend to add
-     */
-    public void addBackend(final Backend backend) {
-        backends.add(backend);
-        if (backend instanceof TWindowBackend) {
-            multiScreen.addScreen(((TWindowBackend) backend).getOtherScreen());
-        } else {
-            multiScreen.addScreen(backend.getScreen());
-        }
-    }
-
-    /**
-     * Remove a backend from the list.
-     *
-     * @param backend the backend to remove
-     */
-    public void removeBackend(final Backend backend) {
-        if (backends.size() > 1) {
-            if (backend instanceof TWindowBackend) {
-                multiScreen.removeScreen(((TWindowBackend) backend).getOtherScreen());
-            } else {
-                multiScreen.removeScreen(backend.getScreen());
-            }
-            backends.remove(backend);
-        }
-    }
+    // ------------------------------------------------------------------------
+    // Backend ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Getter for sessionInfo.
@@ -121,6 +103,20 @@ public class MultiBackend implements Backend {
         }
     }
 
+    /**
+     * Check if there are events in the queue.
+     *
+     * @return if true, getEvents() has something to return to the application
+     */
+    public boolean hasEvents() {
+        for (Backend backend: backends) {
+            if (backend.hasEvents()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Subclasses must provide an implementation to get keyboard, mouse, and
      * screen resize events.
@@ -166,4 +162,38 @@ public class MultiBackend implements Backend {
         }
     }
 
+    // ------------------------------------------------------------------------
+    // MultiBackend -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Add a backend to the list.
+     *
+     * @param backend the backend to add
+     */
+    public void addBackend(final Backend backend) {
+        backends.add(backend);
+        if (backend instanceof TWindowBackend) {
+            multiScreen.addScreen(((TWindowBackend) backend).getOtherScreen());
+        } else {
+            multiScreen.addScreen(backend.getScreen());
+        }
+    }
+
+    /**
+     * Remove a backend from the list.
+     *
+     * @param backend the backend to remove
+     */
+    public void removeBackend(final Backend backend) {
+        if (backends.size() > 1) {
+            if (backend instanceof TWindowBackend) {
+                multiScreen.removeScreen(((TWindowBackend) backend).getOtherScreen());
+            } else {
+                multiScreen.removeScreen(backend.getScreen());
+            }
+            backends.remove(backend);
+        }
+    }
+
 }
index 735faace1a130576b28e6fd8d8f667a9acc36510..77688734fa9179aca6414829a5bc29cdcc91ecf7 100644 (file)
@@ -39,11 +39,19 @@ import jexer.bits.CellAttributes;
  */
 public class MultiScreen implements Screen {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The list of screens to use.
      */
     private List<Screen> screens = new LinkedList<Screen>();
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Public constructor requires one screen.
      *
@@ -53,25 +61,9 @@ public class MultiScreen implements Screen {
         screens.add(screen);
     }
 
-    /**
-     * Add a screen to the list.
-     *
-     * @param screen the screen to add
-     */
-    public void addScreen(final Screen screen) {
-        screens.add(screen);
-    }
-
-    /**
-     * Remove a screen from the list.
-     *
-     * @param screen the screen to remove
-     */
-    public void removeScreen(final Screen screen) {
-        if (screens.size() > 1) {
-            screens.remove(screen);
-        }
-    }
+    // ------------------------------------------------------------------------
+    // Screen -----------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Set drawing offset for x.
@@ -574,4 +566,28 @@ public class MultiScreen implements Screen {
         }
     }
 
+    // ------------------------------------------------------------------------
+    // MultiScreen ------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Add a screen to the list.
+     *
+     * @param screen the screen to add
+     */
+    public void addScreen(final Screen screen) {
+        screens.add(screen);
+    }
+
+    /**
+     * Remove a screen from the list.
+     *
+     * @param screen the screen to remove
+     */
+    public void removeScreen(final Screen screen) {
+        if (screens.size() > 1) {
+            screens.remove(screen);
+        }
+    }
+
 }
index a98627ec3f860d79df7bd71c55be2fab50b63f29..d6e07422d467f0cbe867ed5f755b38ac3357e4ad 100644 (file)
@@ -37,6 +37,10 @@ import javax.swing.JComponent;
  */
 public final class SwingBackend extends GenericBackend {
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Public constructor.  The window will be 80x25 with font size 20 pts.
      */
@@ -127,6 +131,10 @@ public final class SwingBackend extends GenericBackend {
         screen = (SwingTerminal) terminal;
     }
 
+    // ------------------------------------------------------------------------
+    // SwingBackend -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Set to a new font, and resize the screen to match its dimensions.
      *
index 48e4a44fa6b5b9f2db1765ec8f700f78137d38b3..4b0b2b4d079bc2f8b9d43bbc7ad10ab268852438 100644 (file)
@@ -52,11 +52,53 @@ import javax.swing.JFrame;
  */
 class SwingComponent {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * If true, use triple buffering when drawing to a JFrame.
      */
     public static boolean tripleBuffer = true;
 
+    /**
+     * 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;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * 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();
+    }
+
+    // ------------------------------------------------------------------------
+    // SwingComponent ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Get the BufferStrategy object needed for triple-buffering.
      *
@@ -73,16 +115,6 @@ class SwingComponent {
         }
     }
 
-    /**
-     * 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.
      *
@@ -101,26 +133,6 @@ class SwingComponent {
         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.
      */
index 7457f57a0d73b185f1f618e4a035faade6e7c88b..b4c0b59d9f3c6868518dc470a39a7f06fc192401 100644 (file)
@@ -37,6 +37,10 @@ import java.awt.Insets;
  */
 public final class SwingSessionInfo implements SessionInfo {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The Swing JFrame or JComponent.
      */
@@ -72,6 +76,48 @@ public final class SwingSessionInfo implements SessionInfo {
      */
     private int windowHeight = 25;
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @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
+     */
+    public SwingSessionInfo(final SwingComponent swing, final int textWidth,
+        final int textHeight) {
+
+        this.swing      = swing;
+        this.textWidth  = textWidth;
+        this.textHeight = textHeight;
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @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 width the number of columns
+     * @param height the number of rows
+     */
+    public SwingSessionInfo(final SwingComponent swing, final int textWidth,
+        final int textHeight, final int width, final int height) {
+
+        this.swing              = swing;
+        this.textWidth          = textWidth;
+        this.textHeight         = textHeight;
+        this.windowWidth        = width;
+        this.windowHeight       = height;
+    }
+
+    // ------------------------------------------------------------------------
+    // SessionInfo ------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Username getter.
      *
@@ -126,53 +172,6 @@ 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 swing the Swing JFrame or JComponent
-     * @param textWidth the width of a cell in pixels
-     * @param textHeight the height of a cell in pixels
-     */
-    public SwingSessionInfo(final SwingComponent swing, final int textWidth,
-        final int textHeight) {
-
-        this.swing      = swing;
-        this.textWidth  = textWidth;
-        this.textHeight = textHeight;
-    }
-
-    /**
-     * Public constructor.
-     *
-     * @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 width the number of columns
-     * @param height the number of rows
-     */
-    public SwingSessionInfo(final SwingComponent swing, final int textWidth,
-        final int textHeight, final int width, final int height) {
-
-        this.swing              = swing;
-        this.textWidth          = textWidth;
-        this.textHeight         = textHeight;
-        this.windowWidth        = width;
-        this.windowHeight       = height;
-    }
-
     /**
      * Re-query the text window size.
      */
@@ -191,4 +190,21 @@ public final class SwingSessionInfo implements SessionInfo {
 
     }
 
+    // ------------------------------------------------------------------------
+    // SwingSessionInfo -------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * 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;
+    }
+
 }
index a98063867e75479c2b18d91e2872fb2fdadec4a5..598f0ab1bce9f7d834cb770a283500c6aa95b2f2 100644 (file)
@@ -54,8 +54,10 @@ import java.io.InputStream;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import javax.swing.JComponent;
 import javax.swing.JFrame;
+import javax.swing.ImageIcon;
 import javax.swing.SwingUtilities;
 
 import jexer.TKeypress;
@@ -84,14 +86,19 @@ public final class SwingTerminal extends LogicalScreen
                                             MouseListener, MouseMotionListener,
                                             MouseWheelListener, WindowListener {
 
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
-     * The Swing component or frame to draw to.
+     * The icon image location.
      */
-    private SwingComponent swing;
+    private static final String ICONFILE = "jexer_logo_128.png";
 
-    // ------------------------------------------------------------------------
-    // Screen -----------------------------------------------------------------
-    // ------------------------------------------------------------------------
+    /**
+     * The terminus font resource filename.
+     */
+    private static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
 
     /**
      * Cursor style to draw.
@@ -113,17 +120,9 @@ public final class SwingTerminal extends LogicalScreen
         OUTLINE
     }
 
-    /**
-     * A cache of previously-rendered glyphs for blinking text, when it is
-     * not visible.
-     */
-    private HashMap<Cell, BufferedImage> glyphCacheBlink;
-
-    /**
-     * A cache of previously-rendered glyphs for non-blinking, or
-     * blinking-and-visible, text.
-     */
-    private HashMap<Cell, BufferedImage> glyphCache;
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     // Colors to map DOS colors to AWT colors.
     private static Color MYBLACK;
@@ -149,36 +148,21 @@ public final class SwingTerminal extends LogicalScreen
     private static boolean dosColors = false;
 
     /**
-     * Setup Swing colors to match DOS color palette.
+     * The Swing component or frame to draw to.
      */
-    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);
+    private SwingComponent swing;
 
-        dosColors = true;
-    }
+    /**
+     * A cache of previously-rendered glyphs for blinking text, when it is
+     * not visible.
+     */
+    private Map<Cell, BufferedImage> glyphCacheBlink;
 
     /**
-     * The terminus font resource filename.
+     * A cache of previously-rendered glyphs for non-blinking, or
+     * blinking-and-visible, text.
      */
-    private static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
+    private Map<Cell, BufferedImage> glyphCache;
 
     /**
      * If true, we were successful getting Terminus.
@@ -246,17 +230,6 @@ public final class SwingTerminal extends LogicalScreen
      */
     private long blinkMillis = 500;
 
-    /**
-     * Get the number of millis to wait before switching the blink from
-     * visible to invisible.
-     *
-     * @return the number of milli to wait before switching the blink from
-     * visible to invisible
-     */
-    public long getBlinkMillis() {
-        return blinkMillis;
-    }
-
     /**
      * If true, the cursor should be visible right now based on the blink
      * time.
@@ -270,507 +243,321 @@ public final class SwingTerminal extends LogicalScreen
     private long lastBlinkTime = 0;
 
     /**
-     * Get the font size in points.
-     *
-     * @return font size in points
+     * The session information.
      */
-    public int getFontSize() {
-        return fontSize;
-    }
+    private SwingSessionInfo sessionInfo;
 
     /**
-     * Set the font size in points.
-     *
-     * @param fontSize font size in points
+     * The listening object that run() wakes up on new input.
      */
-    public void setFontSize(final int fontSize) {
-        this.fontSize = fontSize;
-        Font newFont = font.deriveFont((float) fontSize);
-        setFont(newFont);
-    }
+    private Object listener;
 
     /**
-     * Set to a new font, and resize the screen to match its dimensions.
-     *
-     * @param font the new font
+     * The event queue, filled up by a thread reading on input.
      */
-    public void setFont(final Font font) {
-        this.font = font;
-        getFontDimensions();
-        swing.setFont(font);
-        glyphCacheBlink = new HashMap<Cell, BufferedImage>();
-        glyphCache = new HashMap<Cell, BufferedImage>();
-        resizeToScreen();
-    }
+    private List<TInputEvent> eventQueue;
 
     /**
-     * Set the font to Terminus, the best all-around font for both CP437 and
-     * ISO8859-1.
+     * The last reported mouse X position.
      */
-    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);
-    }
+    private int oldMouseX = -1;
 
     /**
-     * Convert a CellAttributes foreground color to an Swing Color.
-     *
-     * @param attr the text attributes
-     * @return the Swing Color
+     * The last reported mouse Y position.
      */
-    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());
-    }
+    private int oldMouseY = -1;
 
     /**
-     * Convert a CellAttributes background color to an Swing Color.
-     *
-     * @param attr the text attributes
-     * @return the Swing Color
+     * true if mouse1 was down.  Used to report mouse1 on the release event.
      */
-    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());
-    }
+    private boolean mouse1 = false;
 
     /**
-     * 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
+     * true if mouse2 was down.  Used to report mouse2 on the release event.
      */
-    private boolean getFontAdjustments() {
-        BufferedImage image = null;
+    private boolean mouse2 = false;
 
-        // 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.
+    /**
+     * true if mouse3 was down.  Used to report mouse3 on the release event.
+     */
+    private boolean mouse3 = false;
 
-        Graphics2D gr2 = null;
-        int gr2x = 3;
-        int gr2y = 3;
-        image = new BufferedImage(textWidth * 2, textHeight * 2,
-            BufferedImage.TYPE_INT_ARGB);
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
-        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();
+    /**
+     * 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) {
 
-        for (int x = 0; x < textWidth; x++) {
-            for (int y = 0; y < textHeight; y++) {
+        this.fontSize = fontSize;
 
-                /*
-                System.err.println("X: " + x + " Y: " + y + " " +
-                    image.getRGB(x, y));
-                 */
+        setDOSColors();
 
-                if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
-                    textAdjustY = (gr2y - y);
+        // 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;
+        }
 
-                    // System.err.println("textAdjustY: " + textAdjustY);
-                    x = textWidth;
-                    break;
-                }
+        // Pull the system property for triple buffering.
+        if (System.getProperty("jexer.Swing.tripleBuffer") != null) {
+            if (System.getProperty("jexer.Swing.tripleBuffer").equals("true")) {
+                SwingComponent.tripleBuffer = true;
+            } else {
+                SwingComponent.tripleBuffer = false;
             }
         }
 
-        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();
+        try {
+            SwingUtilities.invokeAndWait(new Runnable() {
+                public void run() {
 
-        for (int x = 0; x < textWidth; x++) {
-            for (int y = 0; y < textHeight; y++) {
+                    JFrame frame = new JFrame() {
 
-                /*
-                System.err.println("X: " + x + " Y: " + y + " " +
-                    image.getRGB(x, y));
-                 */
+                        /**
+                         * Serializable version.
+                         */
+                        private static final long serialVersionUID = 1;
 
-                if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
-                    textAdjustX = (gr2x - x);
+                        /**
+                         * The code that performs the actual drawing.
+                         */
+                        public SwingTerminal screen = null;
 
-                    // System.err.println("textAdjustX: " + textAdjustX);
-                    return true;
-                }
-            }
-        }
+                        /*
+                         * Anonymous class initializer saves the screen
+                         * reference, so that paint() and the like call out
+                         * to SwingTerminal.
+                         */
+                        {
+                            this.screen = SwingTerminal.this;
+                        }
 
-        // Something weird happened, don't rely on this function.
-        // System.err.println("getFontAdjustments: false");
-        return false;
-    }
+                        /**
+                         * 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);
+                        }
 
-    /**
-     * 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);
-    }
+                        /**
+                         * Paint redraws the whole screen.
+                         *
+                         * @param gr the Swing Graphics context
+                         */
+                        @Override
+                        public void paint(final Graphics gr) {
+                            if (screen != null) {
+                                screen.paint(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;
+                    // Set icon
+                    ClassLoader loader = Thread.currentThread().
+                        getContextClassLoader();
+                    frame.setIconImage((new ImageIcon(loader.
+                                getResource(ICONFILE))).getImage());
 
-        // This produces the same number, but works better for ugly
-        // monospace.
-        textHeight = fm.getMaxAscent() + maxDescent - leading;
+                    // Get the Swing component
+                    SwingTerminal.this.swing = new SwingComponent(frame);
 
-        if (gotTerminus == true) {
-            textHeight++;
-        }
+                    // Hang onto top and left for drawing.
+                    Insets insets = SwingTerminal.this.swing.getInsets();
+                    SwingTerminal.this.left = insets.left;
+                    SwingTerminal.this.top = insets.top;
 
-        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;
-            }
-        }
+                    // Load the font so that we can set sessionInfo.
+                    getDefaultFont();
 
-        if (sessionInfo != null) {
-            sessionInfo.setTextCellDimensions(textWidth, textHeight);
+                    // 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,
+                            windowWidth, windowHeight);
+
+                    SwingTerminal.this.setDimensions(sessionInfo.getWindowWidth(),
+                        sessionInfo.getWindowHeight());
+
+                    SwingTerminal.this.resizeToScreen();
+                    SwingTerminal.this.swing.setVisible(true);
+                }
+            });
+        } catch (Exception e) {
+            e.printStackTrace();
         }
-        gotFontDimensions = true;
-    }
 
-    /**
-     * Resize to font dimensions.
-     */
-    public void resizeToScreen() {
-        swing.setDimensions(textWidth * width, textHeight * height);
+        this.listener    = listener;
+        mouse1           = false;
+        mouse2           = false;
+        mouse3           = false;
+        eventQueue       = new LinkedList<TInputEvent>();
+
+        // Add listeners to Swing.
+        swing.addKeyListener(this);
+        swing.addWindowListener(this);
+        swing.addComponentListener(this);
+        swing.addMouseListener(this);
+        swing.addMouseMotionListener(this);
+        swing.addMouseWheelListener(this);
     }
 
     /**
-     * Draw one glyph to the screen.
+     * Public constructor renders to an existing JComponent.
      *
-     * @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.
+     * @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
      */
-    private void drawGlyph(final Graphics gr, final Cell cell,
-        final int xPixel, final int yPixel) {
+    public SwingTerminal(final JComponent component, final int windowWidth,
+        final int windowHeight, final int fontSize, final Object listener) {
 
-        /*
-        System.err.println("drawGlyph(): " + xPixel + " " + yPixel +
-            " " + cell);
-        */
+        this.fontSize = fontSize;
 
-        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;
-        }
+        setDOSColors();
 
-        // 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;
+        // 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;
         }
 
-        Cell cellColor = new Cell();
-        cellColor.setTo(cell);
+        try {
+            SwingUtilities.invokeAndWait(new Runnable() {
+                public void run() {
 
-        // Check for reverse
-        if (cell.isReverse()) {
-            cellColor.setForeColor(cell.getBackColor());
-            cellColor.setBackColor(cell.getForeColor());
-        }
+                    JComponent newComponent = new JComponent() {
 
-        // Draw the background rectangle, then the foreground character.
-        gr2.setColor(attrToBackgroundColor(cellColor));
-        gr2.fillRect(gr2x, gr2y, textWidth, textHeight);
+                        /**
+                         * Serializable version.
+                         */
+                        private static final long serialVersionUID = 1;
 
-        // 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);
+                        /**
+                         * The code that performs the actual drawing.
+                         */
+                        public SwingTerminal screen = null;
 
-            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());
-            }
-        }
-
-    }
+                        /*
+                         * Anonymous class initializer saves the screen
+                         * reference, so that paint() and the like call out
+                         * to SwingTerminal.
+                         */
+                        {
+                            this.screen = SwingTerminal.this;
+                        }
 
-    /**
-     * Check if the cursor is visible, and if so draw it.
-     *
-     * @param gr the Swing Graphics context
-     */
-    private void drawCursor(final Graphics gr) {
+                        /**
+                         * 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);
+                        }
 
-        if (cursorVisible
-            && (cursorY >= 0)
-            && (cursorX >= 0)
-            && (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
+                         */
+                        @Override
+                        public void paint(final Graphics gr) {
+                            if (screen != null) {
+                                screen.paint(gr);
+                            }
+                        }
+                    };
+                    component.setLayout(new BorderLayout());
+                    component.add(newComponent);
 
-    /**
-     * Reset the blink timer.
-     */
-    private void resetBlinkTimer() {
-        lastBlinkTime = System.currentTimeMillis();
-        cursorBlinkVisible = true;
-    }
+                    // Allow key events to be received
+                    component.setFocusable(true);
 
-    /**
-     * Paint redraws the whole screen.
-     *
-     * @param gr the Swing Graphics context
-     */
-    public void paint(final Graphics gr) {
+                    // Get the Swing component
+                    SwingTerminal.this.swing = new SwingComponent(component);
 
-        if (gotFontDimensions == false) {
-            // Lazy-load the text width/height
-            getFontDimensions(gr);
-            /*
-            System.err.println("textWidth " + textWidth +
-                " textHeight " + textHeight);
-            System.err.println("FONT: " + swing.getFont() + " font " + font);
-             */
-        }
+                    // Hang onto top and left for drawing.
+                    Insets insets = SwingTerminal.this.swing.getInsets();
+                    SwingTerminal.this.left = insets.left;
+                    SwingTerminal.this.top = insets.top;
 
-        int xCellMin = 0;
-        int xCellMax = width;
-        int yCellMin = 0;
-        int yCellMax = height;
+                    // Load the font so that we can set sessionInfo.
+                    getDefaultFont();
 
-        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;
+                    // 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();
         }
 
-        // 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);
+        this.listener    = listener;
+        mouse1           = false;
+        mouse2           = false;
+        mouse3           = false;
+        eventQueue       = new LinkedList<TInputEvent>();
 
-            reallyCleared = false;
-        } // synchronized (this)
+        // Add listeners to Swing.
+        swing.addKeyListener(this);
+        swing.addWindowListener(this);
+        swing.addComponentListener(this);
+        swing.addMouseListener(this);
+        swing.addMouseMotionListener(this);
+        swing.addMouseWheelListener(this);
     }
 
+    // ------------------------------------------------------------------------
+    // LogicalScreen ----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
-     * Restore terminal to normal state.
+     * Set the window title.
+     *
+     * @param title the new title
      */
-    public void shutdown() {
-        swing.dispose();
+    @Override
+    public void setTitle(final String title) {
+        swing.setTitle(title);
     }
 
     /**
@@ -804,520 +591,787 @@ public final class SwingTerminal extends LogicalScreen
         }
     }
 
+    // ------------------------------------------------------------------------
+    // TerminalReader ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
-     * Push the logical screen to the physical device.
+     * Check if there are events in the queue.
+     *
+     * @return if true, getEvents() has something to return to the backend
      */
-    private void drawToSwing() {
+    public boolean hasEvents() {
+        synchronized (eventQueue) {
+            return (eventQueue.size() > 0);
+        }
+    }
 
-        /*
-        System.err.printf("drawToSwing(): reallyCleared %s dirty %s\n",
-            reallyCleared, dirty);
-        */
+    /**
+     * Return any events in the IO queue.
+     *
+     * @param queue list to append new events to
+     */
+    public void getEvents(final List<TInputEvent> queue) {
+        synchronized (eventQueue) {
+            if (eventQueue.size() > 0) {
+                synchronized (queue) {
+                    queue.addAll(eventQueue);
+                }
+                eventQueue.clear();
+            }
+        }
+    }
 
-        // 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();
-            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();
+    /**
+     * Restore terminal to normal state.
+     */
+    public void closeTerminal() {
+        shutdown();
+    }
+
+    /**
+     * 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;
+    }
+
+    // ------------------------------------------------------------------------
+    // SwingTerminal ----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * 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);
 
-        if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) {
-            Graphics gr = swing.getBufferStrategy().getDrawGraphics();
+        dosColors = true;
+    }
 
-            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];
+    /**
+     * Get the number of millis to wait before switching the blink from
+     * visible to invisible.
+     *
+     * @return the number of milli to wait before switching the blink from
+     * visible to invisible
+     */
+    public long getBlinkMillis() {
+        return blinkMillis;
+    }
 
-                        int xPixel = x * textWidth + left;
-                        int yPixel = y * textHeight + top;
+    /**
+     * Get the font size in points.
+     *
+     * @return font size in points
+     */
+    public int getFontSize() {
+        return fontSize;
+    }
 
-                        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)
+    /**
+     * 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);
+    }
 
-            gr.dispose();
-            swing.getBufferStrategy().show();
-            Toolkit.getDefaultToolkit().sync();
-            return;
+    /**
+     * 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<Cell, BufferedImage>();
+        glyphCache = new HashMap<Cell, BufferedImage>();
+        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;
+                }
+            }
         }
 
-        // Swing thread version: request a repaint, but limit it to the area
-        // that has changed.
+        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();
 
-        // Find the minimum-size damaged region.
-        int xMin = swing.getWidth();
-        int xMax = 0;
-        int yMin = swing.getHeight();
-        int yMax = 0;
+        for (int x = 0; x < textWidth; x++) {
+            for (int y = 0; y < textHeight; y++) {
 
-        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];
+                /*
+                System.err.println("X: " + x + " Y: " + y + " " +
+                    image.getRGB(x, y));
+                 */
 
-                    int xPixel = x * textWidth + left;
-                    int yPixel = y * textHeight + top;
+                if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
+                    textAdjustX = (gr2x - x);
 
-                    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;
-                        }
-                    }
+                    // System.err.println("textAdjustX: " + textAdjustX);
+                    return true;
                 }
             }
         }
-        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();
-            Toolkit.getDefaultToolkit().sync();
-        } else {
-            // Repaint on the Swing thread.
-            swing.repaint(xMin, yMin, xMax - xMin, yMax - yMin);
-        }
+        // Something weird happened, don't rely on this function.
+        // System.err.println("getFontAdjustments: false");
+        return false;
     }
 
     /**
-     * Convert pixel column position to text cell column position.
-     *
-     * @param x pixel column position
-     * @return text cell column position
+     * Figure out my font dimensions.  This code path works OK for the JFrame
+     * case, and can be called immediately after JFrame creation.
      */
-    public int textColumn(final int x) {
-        return ((x - left) / textWidth);
+    private void getFontDimensions() {
+        swing.setFont(font);
+        Graphics gr = swing.getGraphics();
+        if (gr == null) {
+            return;
+        }
+        getFontDimensions(gr);
     }
 
     /**
-     * Convert pixel row position to text cell row position.
+     * Figure out my font dimensions.  This code path is needed to lazy-load
+     * the information inside paint().
      *
-     * @param y pixel row position
-     * @return text cell row position
+     * @param gr Graphics object to use
      */
-    public int textRow(final int y) {
-        return ((y - top) / textHeight);
-    }
+    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;
 
-    /**
-     * Set the window title.
-     *
-     * @param title the new title
-     */
-    public void setTitle(final String title) {
-        swing.setTitle(title);
-    }
+        // This produces the same number, but works better for ugly
+        // monospace.
+        textHeight = fm.getMaxAscent() + maxDescent - leading;
 
-    // ------------------------------------------------------------------------
-    // TerminalReader ---------------------------------------------------------
-    // ------------------------------------------------------------------------
+        if (gotTerminus == true) {
+            textHeight++;
+        }
 
-    /**
-     * The session information.
-     */
-    private SwingSessionInfo sessionInfo;
+        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;
+            }
+        }
 
-    /**
-     * Getter for sessionInfo.
-     *
-     * @return the SessionInfo
-     */
-    public SessionInfo getSessionInfo() {
-        return sessionInfo;
+        if (sessionInfo != null) {
+            sessionInfo.setTextCellDimensions(textWidth, textHeight);
+        }
+        gotFontDimensions = true;
     }
 
     /**
-     * The listening object that run() wakes up on new input.
+     * Resize to font dimensions.
      */
-    private Object listener;
+    public void resizeToScreen() {
+        swing.setDimensions(textWidth * width, textHeight * height);
+    }
 
     /**
-     * Set listener to a different Object.
+     * Draw one glyph to the screen.
      *
-     * @param listener the new listening object that run() wakes up on new
-     * input
+     * @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.
      */
-    public void setListener(final Object listener) {
-        this.listener = listener;
-    }
+    private void drawGlyph(final Graphics gr, final Cell cell,
+        final int xPixel, final int yPixel) {
 
-    /**
-     * The event queue, filled up by a thread reading on input.
-     */
-    private List<TInputEvent> eventQueue;
+        /*
+        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);
+            }
 
-    /**
-     * The last reported mouse X position.
-     */
-    private int oldMouseX = -1;
+            if (swing.getFrame() != null) {
+                gr.drawImage(image, xPixel, yPixel, swing.getFrame());
+            } else {
+                gr.drawImage(image, xPixel, yPixel, swing.getComponent());
+            }
+        }
 
-    /**
-     * The last reported mouse Y position.
-     */
-    private int oldMouseY = -1;
+    }
 
     /**
-     * true if mouse1 was down.  Used to report mouse1 on the release event.
+     * Check if the cursor is visible, and if so draw it.
+     *
+     * @param gr the Swing Graphics context
      */
-    private boolean mouse1 = false;
+    private void drawCursor(final Graphics gr) {
 
-    /**
-     * true if mouse2 was down.  Used to report mouse2 on the release event.
-     */
-    private boolean mouse2 = false;
+        if (cursorVisible
+            && (cursorY >= 0)
+            && (cursorX >= 0)
+            && (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;
+            }
+        }
+    }
 
     /**
-     * true if mouse3 was down.  Used to report mouse3 on the release event.
+     * Reset the blink timer.
      */
-    private boolean mouse3 = false;
+    private void resetBlinkTimer() {
+        lastBlinkTime = System.currentTimeMillis();
+        cursorBlinkVisible = true;
+    }
 
     /**
-     * Public constructor creates a new JFrame to render to.
+     * Paint redraws the whole screen.
      *
-     * @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
+     * @param gr the Swing Graphics context
      */
-    public SwingTerminal(final int windowWidth, final int windowHeight,
-        final int fontSize, final Object listener) {
-
-        this.fontSize = fontSize;
-
-        setDOSColors();
+    public void paint(final Graphics gr) {
 
-        // 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 (gotFontDimensions == false) {
+            // Lazy-load the text width/height
+            getFontDimensions(gr);
+            /*
+            System.err.println("textWidth " + textWidth +
+                " textHeight " + textHeight);
+            System.err.println("FONT: " + swing.getFont() + " font " + font);
+             */
         }
 
-        // Pull the system property for triple buffering.
-        if (System.getProperty("jexer.Swing.tripleBuffer") != null) {
-            if (System.getProperty("jexer.Swing.tripleBuffer").equals("true")) {
-                SwingComponent.tripleBuffer = true;
-            } else {
-                SwingComponent.tripleBuffer = false;
-            }
+        if ((swing.getBufferStrategy() != null)
+            && (SwingUtilities.isEventDispatchThread())
+        ) {
+            // System.err.println("paint(), skip first paint on swing thread");
+            return;
         }
 
-        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;
+        int xCellMin = 0;
+        int xCellMax = width;
+        int yCellMin = 0;
+        int yCellMax = height;
 
-                        /*
-                         * Anonymous class initializer saves the screen
-                         * reference, so that paint() and the like call out
-                         * to SwingTerminal.
-                         */
-                        {
-                            this.screen = SwingTerminal.this;
-                        }
+        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;
+        }
 
-                        /**
-                         * 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);
-                        }
+        // Prevent updates to the screen's data from the TApplication
+        // threads.
+        synchronized (this) {
 
-                        /**
-                         * Paint redraws the whole screen.
-                         *
-                         * @param gr the Swing Graphics context
-                         */
-                        @Override
-                        public void paint(final Graphics gr) {
-                            if (screen != null) {
-                                screen.paint(gr);
-                            }
-                        }
-                    };
+            /*
+            System.err.printf("bounds %s X %d %d Y %d %d\n",
+                 bounds, xCellMin, xCellMax, yCellMin, yCellMax);
+            */
 
-                    // Get the Swing component
-                    SwingTerminal.this.swing = new SwingComponent(frame);
+            for (int y = yCellMin; y < yCellMax; y++) {
+                for (int x = xCellMin; x < xCellMax; x++) {
 
-                    // Hang onto top and left for drawing.
-                    Insets insets = SwingTerminal.this.swing.getInsets();
-                    SwingTerminal.this.left = insets.left;
-                    SwingTerminal.this.top = insets.top;
+                    int xPixel = x * textWidth + left;
+                    int yPixel = y * textHeight + top;
 
-                    // Load the font so that we can set sessionInfo.
-                    getDefaultFont();
+                    Cell lCell = logical[x][y];
+                    Cell pCell = physical[x][y];
 
-                    // 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,
-                            windowWidth, windowHeight);
+                    if (!lCell.equals(pCell)
+                        || lCell.isBlink()
+                        || reallyCleared
+                        || (swing.getFrame() == null)) {
 
-                    SwingTerminal.this.setDimensions(sessionInfo.getWindowWidth(),
-                        sessionInfo.getWindowHeight());
+                        drawGlyph(gr, lCell, xPixel, yPixel);
 
-                    SwingTerminal.this.resizeToScreen();
-                    SwingTerminal.this.swing.setVisible(true);
+                        // Physical is always updated
+                        physical[x][y].setTo(lCell);
+                    }
                 }
-            });
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-
-        this.listener    = listener;
-        mouse1           = false;
-        mouse2           = false;
-        mouse3           = false;
-        eventQueue       = new LinkedList<TInputEvent>();
+            }
+            drawCursor(gr);
 
-        // Add listeners to Swing.
-        swing.addKeyListener(this);
-        swing.addWindowListener(this);
-        swing.addComponentListener(this);
-        swing.addMouseListener(this);
-        swing.addMouseMotionListener(this);
-        swing.addMouseWheelListener(this);
+            reallyCleared = false;
+        } // synchronized (this)
     }
 
     /**
-     * Public constructor renders to an existing JComponent.
-     *
-     * @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
+     * Restore terminal to normal state.
      */
-    public SwingTerminal(final JComponent component, final int windowWidth,
-        final int windowHeight, final int fontSize, final Object listener) {
+    public void shutdown() {
+        swing.dispose();
+    }
 
-        this.fontSize = fontSize;
+    /**
+     * Push the logical screen to the physical device.
+     */
+    private void drawToSwing() {
 
-        setDOSColors();
+        /*
+        System.err.printf("drawToSwing(): reallyCleared %s dirty %s\n",
+            reallyCleared, dirty);
+        */
 
-        // 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 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();
+            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;
         }
 
-        try {
-            SwingUtilities.invokeAndWait(new Runnable() {
-                public void run() {
-
-                    JComponent newComponent = new JComponent() {
-
-                        /**
-                         * Serializable version.
-                         */
-                        private static final long serialVersionUID = 1;
+        if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) {
+            Graphics gr = swing.getBufferStrategy().getDrawGraphics();
 
-                        /**
-                         * The code that performs the actual drawing.
-                         */
-                        public SwingTerminal screen = null;
+            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];
 
-                        /*
-                         * Anonymous class initializer saves the screen
-                         * reference, so that paint() and the like call out
-                         * to SwingTerminal.
-                         */
-                        {
-                            this.screen = SwingTerminal.this;
-                        }
+                        int xPixel = x * textWidth + left;
+                        int yPixel = y * textHeight + top;
 
-                        /**
-                         * 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);
+                        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)
 
-                        /**
-                         * 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);
+            gr.dispose();
+            swing.getBufferStrategy().show();
+            Toolkit.getDefaultToolkit().sync();
+            return;
+        }
 
-                    // Allow key events to be received
-                    component.setFocusable(true);
+        // Swing thread version: request a repaint, but limit it to the area
+        // that has changed.
 
-                    // Get the Swing component
-                    SwingTerminal.this.swing = new SwingComponent(component);
+        // Find the minimum-size damaged region.
+        int xMin = swing.getWidth();
+        int xMax = 0;
+        int yMin = swing.getHeight();
+        int yMax = 0;
 
-                    // Hang onto top and left for drawing.
-                    Insets insets = SwingTerminal.this.swing.getInsets();
-                    SwingTerminal.this.left = insets.left;
-                    SwingTerminal.this.top = insets.top;
+        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];
 
-                    // Load the font so that we can set sessionInfo.
-                    getDefaultFont();
+                    int xPixel = x * textWidth + left;
+                    int yPixel = y * textHeight + top;
 
-                    // 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);
+                    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;
+                        }
+                    }
                 }
-            });
-        } catch (Exception e) {
-            e.printStackTrace();
+            }
+        }
+        if (xMin + textWidth >= xMax) {
+            xMax += textWidth;
+        }
+        if (yMin + textHeight >= yMax) {
+            yMax += textHeight;
         }
 
-        this.listener    = listener;
-        mouse1           = false;
-        mouse2           = false;
-        mouse3           = false;
-        eventQueue       = new LinkedList<TInputEvent>();
+        // Repaint the desired area
+        /*
+        System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
+            yMin, yMax);
+        */
 
-        // Add listeners to Swing.
-        swing.addKeyListener(this);
-        swing.addWindowListener(this);
-        swing.addComponentListener(this);
-        swing.addMouseListener(this);
-        swing.addMouseMotionListener(this);
-        swing.addMouseWheelListener(this);
+        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();
+            Toolkit.getDefaultToolkit().sync();
+        } else {
+            // Repaint on the Swing thread.
+            swing.repaint(xMin, yMin, xMax - xMin, yMax - yMin);
+        }
     }
 
     /**
-     * Check if there are events in the queue.
+     * Convert pixel column position to text cell column position.
      *
-     * @return if true, getEvents() has something to return to the backend
+     * @param x pixel column position
+     * @return text cell column position
      */
-    public boolean hasEvents() {
-        synchronized (eventQueue) {
-            return (eventQueue.size() > 0);
+    public int textColumn(final int x) {
+        int column = ((x - left) / textWidth);
+        if (column < 0) {
+            column = 0;
         }
+        if (column > width - 1) {
+            column = width - 1;
+        }
+        return column;
     }
 
     /**
-     * Return any events in the IO queue.
+     * Convert pixel row position to text cell row position.
      *
-     * @param queue list to append new events to
+     * @param y pixel row position
+     * @return text cell row position
      */
-    public void getEvents(final List<TInputEvent> queue) {
-        synchronized (eventQueue) {
-            if (eventQueue.size() > 0) {
-                synchronized (queue) {
-                    queue.addAll(eventQueue);
-                }
-                eventQueue.clear();
-            }
+    public int textRow(final int y) {
+        int row = ((y - top) / textHeight);
+        if (row < 0) {
+            row = 0;
+        }
+        if (row > height - 1) {
+            row = height - 1;
         }
+        return row;
     }
 
     /**
-     * Restore terminal to normal state.
+     * Getter for sessionInfo.
+     *
+     * @return the SessionInfo
      */
-    public void closeTerminal() {
-        shutdown();
+    public SessionInfo getSessionInfo() {
+        return sessionInfo;
     }
 
+    // ------------------------------------------------------------------------
+    // KeyListener ------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Pass Swing keystrokes into the event queue.
      *
@@ -1550,6 +1604,10 @@ public final class SwingTerminal extends LogicalScreen
         }
     }
 
+    // ------------------------------------------------------------------------
+    // WindowListener ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Pass window events into the event queue.
      *
@@ -1625,6 +1683,10 @@ public final class SwingTerminal extends LogicalScreen
         // Ignore
     }
 
+    // ------------------------------------------------------------------------
+    // ComponentListener ------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Pass component events into the event queue.
      *
@@ -1672,6 +1734,10 @@ public final class SwingTerminal extends LogicalScreen
                 sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
             eventQueue.add(windowResize);
             resetBlinkTimer();
+            /*
+            System.err.println("Add resize event: " + windowResize.getWidth() +
+                " x " + windowResize.getHeight());
+             */
         }
         if (listener != null) {
             synchronized (listener) {
@@ -1680,6 +1746,10 @@ public final class SwingTerminal extends LogicalScreen
         }
     }
 
+    // ------------------------------------------------------------------------
+    // MouseMotionListener ----------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Pass mouse events into the event queue.
      *
@@ -1748,6 +1818,10 @@ public final class SwingTerminal extends LogicalScreen
         }
     }
 
+    // ------------------------------------------------------------------------
+    // MouseListener ----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Pass mouse events into the event queue.
      *
@@ -1862,6 +1936,10 @@ public final class SwingTerminal extends LogicalScreen
         }
     }
 
+    // ------------------------------------------------------------------------
+    // MouseWheelListener -----------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Pass mouse events into the event queue.
      *
index bc32175398ddc8c77d91a08deb4c7824710baa30..63c12f565a9c24f63d6846ca92a09c4cdce77b45 100644 (file)
@@ -34,6 +34,10 @@ package jexer.backend;
  */
 public final class TSessionInfo implements SessionInfo {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * User name.
      */
@@ -54,6 +58,32 @@ public final class TSessionInfo implements SessionInfo {
      */
     private int windowHeight = 24;
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     */
+    public TSessionInfo() {
+        this(80, 24);
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param width the number of columns
+     * @param height the number of rows
+     */
+    public TSessionInfo(final int width, final int height) {
+        this.windowWidth        = width;
+        this.windowHeight       = height;
+    }
+
+    // ------------------------------------------------------------------------
+    // SessionInfo ------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Username getter.
      *
@@ -115,22 +145,4 @@ public final class TSessionInfo implements SessionInfo {
         // NOP
     }
 
-    /**
-     * Public constructor.
-     */
-    public TSessionInfo() {
-        this(80, 24);
-    }
-
-    /**
-     * Public constructor.
-     *
-     * @param width the number of columns
-     * @param height the number of rows
-     */
-    public TSessionInfo(final int width, final int height) {
-        this.windowWidth        = width;
-        this.windowHeight       = height;
-    }
-
 }
index 67b2e5c4b9f357e125cb4077ec1e38a0d76e49fc..28b4bd65d793f3652e69865155bbc61e07fcf1fb 100644 (file)
@@ -40,6 +40,10 @@ import java.util.StringTokenizer;
  */
 public final class TTYSessionInfo implements SessionInfo {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * User name.
      */
@@ -65,6 +69,24 @@ public final class TTYSessionInfo implements SessionInfo {
      */
     private long lastQueryWindowTime;
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     */
+    public TTYSessionInfo() {
+        // Populate lang and user from the environment
+        username = System.getProperty("user.name");
+        language = System.getProperty("user.language");
+        queryWindowSize();
+    }
+
+    // ------------------------------------------------------------------------
+    // SessionInfo ------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Username getter.
      *
@@ -101,54 +123,6 @@ public final class TTYSessionInfo implements SessionInfo {
         this.language = language;
     }
 
-    /**
-     * Call 'stty size' to obtain the tty window size.  windowWidth and
-     * windowHeight are set automatically.
-     */
-    private void sttyWindowSize() {
-        String [] cmd = {
-            "/bin/sh", "-c", "stty size < /dev/tty"
-        };
-        try {
-            Process process = Runtime.getRuntime().exec(cmd);
-            BufferedReader in = new BufferedReader(
-                new InputStreamReader(process.getInputStream(), "UTF-8"));
-            String line = in.readLine();
-            if ((line != null) && (line.length() > 0)) {
-                StringTokenizer tokenizer = new StringTokenizer(line);
-                int rc = Integer.parseInt(tokenizer.nextToken());
-                if (rc > 0) {
-                    windowHeight = rc;
-                }
-                rc = Integer.parseInt(tokenizer.nextToken());
-                if (rc > 0) {
-                    windowWidth = rc;
-                }
-            }
-            while (true) {
-                BufferedReader err = new BufferedReader(
-                        new InputStreamReader(process.getErrorStream(),
-                            "UTF-8"));
-                line = err.readLine();
-                if ((line != null) && (line.length() > 0)) {
-                    System.err.println("Error output from stty: " + line);
-                }
-                try {
-                    process.waitFor();
-                    break;
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-            }
-            int rc = process.exitValue();
-            if (rc != 0) {
-                System.err.println("stty returned error code: " + rc);
-            }
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
-    }
-
     /**
      * Text window width getter.
      *
@@ -199,13 +173,56 @@ public final class TTYSessionInfo implements SessionInfo {
         }
     }
 
+    // ------------------------------------------------------------------------
+    // TTYSessionInfo ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
-     * Public constructor.
+     * Call 'stty size' to obtain the tty window size.  windowWidth and
+     * windowHeight are set automatically.
      */
-    public TTYSessionInfo() {
-        // Populate lang and user from the environment
-        username = System.getProperty("user.name");
-        language = System.getProperty("user.language");
-        queryWindowSize();
+    private void sttyWindowSize() {
+        String [] cmd = {
+            "/bin/sh", "-c", "stty size < /dev/tty"
+        };
+        try {
+            Process process = Runtime.getRuntime().exec(cmd);
+            BufferedReader in = new BufferedReader(
+                new InputStreamReader(process.getInputStream(), "UTF-8"));
+            String line = in.readLine();
+            if ((line != null) && (line.length() > 0)) {
+                StringTokenizer tokenizer = new StringTokenizer(line);
+                int rc = Integer.parseInt(tokenizer.nextToken());
+                if (rc > 0) {
+                    windowHeight = rc;
+                }
+                rc = Integer.parseInt(tokenizer.nextToken());
+                if (rc > 0) {
+                    windowWidth = rc;
+                }
+            }
+            while (true) {
+                BufferedReader err = new BufferedReader(
+                        new InputStreamReader(process.getErrorStream(),
+                            "UTF-8"));
+                line = err.readLine();
+                if ((line != null) && (line.length() > 0)) {
+                    System.err.println("Error output from stty: " + line);
+                }
+                try {
+                    process.waitFor();
+                    break;
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }
+            int rc = process.exitValue();
+            if (rc != 0) {
+                System.err.println("stty returned error code: " + rc);
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
     }
+
 }
index 7652d3f2300811f5e349916bc214f987714259f3..41809cff5c5cd0452b8d9d508de8c02fe9d3bfb9 100644 (file)
@@ -48,6 +48,10 @@ import jexer.TWindow;
  */
 public class TWindowBackend extends TWindow implements Backend {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The listening object that run() wakes up on new input.
      */
@@ -84,32 +88,9 @@ public class TWindowBackend extends TWindow implements Backend {
      */
     private SessionInfo sessionInfo;
 
-    /**
-     * Set the object to sync to in draw().
-     *
-     * @param drawLock the object to synchronize on
-     */
-    public void setDrawLock(final Object drawLock) {
-        this.drawLock = drawLock;
-    }
-
-    /**
-     * Getter for the other application's screen.
-     *
-     * @return the Screen
-     */
-    public Screen getOtherScreen() {
-        return otherScreen;
-    }
-
-    /**
-     * Getter for sessionInfo.
-     *
-     * @return the SessionInfo
-     */
-    public final SessionInfo getSessionInfo() {
-        return sessionInfo;
-    }
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Public constructor.  Window will be located at (0, 0).
@@ -214,97 +195,9 @@ public class TWindowBackend extends TWindow implements Backend {
         drawLock = otherScreen;
     }
 
-    /**
-     * Subclasses must provide an implementation that syncs the logical
-     * screen to the physical device.
-     */
-    public void flushScreen() {
-        getApplication().doRepaint();
-    }
-
-    /**
-     * Subclasses must provide an implementation to get keyboard, mouse, and
-     * screen resize events.
-     *
-     * @param queue list to append new events to
-     */
-    public void getEvents(List<TInputEvent> queue) {
-        synchronized (eventQueue) {
-            if (eventQueue.size() > 0) {
-                synchronized (queue) {
-                    queue.addAll(eventQueue);
-                }
-                eventQueue.clear();
-            }
-        }
-    }
-
-    /**
-     * Subclasses must provide an implementation that closes sockets,
-     * restores console, etc.
-     */
-    public void shutdown() {
-        // NOP
-    }
-
-    /**
-     * 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;
-    }
-
-    /**
-     * Draw the foreground colors grid.
-     */
-    @Override
-    public void draw() {
-
-        // Sync on other screen, so that we do not draw in the middle of
-        // their screen update.
-        synchronized (drawLock) {
-            // Draw the box
-            super.draw();
-
-            // Draw every cell of the other screen
-            for (int y = 0; y < otherScreen.getHeight(); y++) {
-                for (int x = 0; x < otherScreen.getWidth(); x++) {
-                    putCharXY(x + 1, y + 1, otherScreen.getCharXY(x, y));
-                }
-            }
-
-            // If the mouse pointer is over the other window, draw its
-            // pointer again here.  (Their TApplication drew it, then our
-            // TApplication drew it again (undo-ing it), so now we draw it a
-            // third time so that it is visible.)
-            if ((otherMouseX != -1) && (otherMouseY != -1)) {
-                CellAttributes attr = getAttrXY(otherMouseX, otherMouseY);
-                attr.setForeColor(attr.getForeColor().invert());
-                attr.setBackColor(attr.getBackColor().invert());
-                putAttrXY(otherMouseX, otherMouseY, attr, false);
-            }
-
-            // If their cursor is visible, draw that here too.
-            if (otherScreen.isCursorVisible()) {
-                setCursorX(otherScreen.getCursorX() + 1);
-                setCursorY(otherScreen.getCursorY() + 1);
-                setCursorVisible(true);
-            } else {
-                setCursorVisible(false);
-            }
-        }
-    }
-
-    /**
-     * Subclasses should override this method to cleanup resources.  This is
-     * called by application.closeWindow().
-     */
-    public void onClose() {
-        // TODO: send a screen disconnect
-    }
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Returns true if the mouse is currently in the otherScreen window.
@@ -413,4 +306,147 @@ public class TWindowBackend extends TWindow implements Backend {
         }
     }
 
+    // ------------------------------------------------------------------------
+    // TWindow ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw the foreground colors grid.
+     */
+    @Override
+    public void draw() {
+
+        // Sync on other screen, so that we do not draw in the middle of
+        // their screen update.
+        synchronized (drawLock) {
+            // Draw the box
+            super.draw();
+
+            // Draw every cell of the other screen
+            for (int y = 0; y < otherScreen.getHeight(); y++) {
+                for (int x = 0; x < otherScreen.getWidth(); x++) {
+                    putCharXY(x + 1, y + 1, otherScreen.getCharXY(x, y));
+                }
+            }
+
+            // If the mouse pointer is over the other window, draw its
+            // pointer again here.  (Their TApplication drew it, then our
+            // TApplication drew it again (undo-ing it), so now we draw it a
+            // third time so that it is visible.)
+            if ((otherMouseX != -1) && (otherMouseY != -1)) {
+                CellAttributes attr = getAttrXY(otherMouseX, otherMouseY);
+                attr.setForeColor(attr.getForeColor().invert());
+                attr.setBackColor(attr.getBackColor().invert());
+                putAttrXY(otherMouseX, otherMouseY, attr, false);
+            }
+
+            // If their cursor is visible, draw that here too.
+            if (otherScreen.isCursorVisible()) {
+                setCursorX(otherScreen.getCursorX() + 1);
+                setCursorY(otherScreen.getCursorY() + 1);
+                setCursorVisible(true);
+            } else {
+                setCursorVisible(false);
+            }
+        }
+    }
+
+    /**
+     * Subclasses should override this method to cleanup resources.  This is
+     * called by application.closeWindow().
+     */
+    @Override
+    public void onClose() {
+        // TODO: send a screen disconnect
+    }
+
+    // ------------------------------------------------------------------------
+    // Backend ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Getter for sessionInfo.
+     *
+     * @return the SessionInfo
+     */
+    public final SessionInfo getSessionInfo() {
+        return sessionInfo;
+    }
+
+    /**
+     * Subclasses must provide an implementation that syncs the logical
+     * screen to the physical device.
+     */
+    public void flushScreen() {
+        getApplication().doRepaint();
+    }
+
+    /**
+     * Check if there are events in the queue.
+     *
+     * @return if true, getEvents() has something to return to the application
+     */
+    public boolean hasEvents() {
+        synchronized (eventQueue) {
+            return (eventQueue.size() > 0);
+        }
+    }
+
+    /**
+     * Subclasses must provide an implementation to get keyboard, mouse, and
+     * screen resize events.
+     *
+     * @param queue list to append new events to
+     */
+    public void getEvents(List<TInputEvent> queue) {
+        synchronized (eventQueue) {
+            if (eventQueue.size() > 0) {
+                synchronized (queue) {
+                    queue.addAll(eventQueue);
+                }
+                eventQueue.clear();
+            }
+        }
+    }
+
+    /**
+     * Subclasses must provide an implementation that closes sockets,
+     * restores console, etc.
+     */
+    public void shutdown() {
+        // NOP
+    }
+
+    /**
+     * 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;
+    }
+
+    // ------------------------------------------------------------------------
+    // TWindowBackend ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Set the object to sync to in draw().
+     *
+     * @param drawLock the object to synchronize on
+     */
+    public void setDrawLock(final Object drawLock) {
+        this.drawLock = drawLock;
+    }
+
+    /**
+     * Getter for the other application's screen.
+     *
+     * @return the Screen
+     */
+    public Screen getOtherScreen() {
+        return otherScreen;
+    }
+
 }
index eaaab6b7695c5a17b16d775545e0264df8a5df8c..56633e779b245a0b2ba90df3e34d7d51414af851 100644 (file)
@@ -33,11 +33,45 @@ package jexer.bits;
  */
 public final class Cell extends CellAttributes {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The character at this cell.
      */
     private char ch;
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor sets default values of the cell to blank.
+     *
+     * @see #isBlank()
+     * @see #reset()
+     */
+    public Cell() {
+        reset();
+    }
+
+    /**
+     * Public constructor sets the character.  Attributes are the same as
+     * default.
+     *
+     * @param ch character to set to
+     * @see #reset()
+     */
+    public Cell(final char ch) {
+        reset();
+        this.ch = ch;
+    }
+
+    // ------------------------------------------------------------------------
+    // Cell -------------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Getter for cell character.
      *
@@ -149,28 +183,6 @@ public final class Cell extends CellAttributes {
         super.setTo(that);
     }
 
-    /**
-     * Public constructor sets default values of the cell to blank.
-     *
-     * @see #isBlank()
-     * @see #reset()
-     */
-    public Cell() {
-        reset();
-    }
-
-    /**
-     * Public constructor sets the character.  Attributes are the same as
-     * default.
-     *
-     * @param ch character to set to
-     * @see #reset()
-     */
-    public Cell(final char ch) {
-        reset();
-        this.ch = ch;
-    }
-
     /**
      * Make human-readable description of this Cell.
      *
index c14557679ee4ff28298021d492b502661ebd0e2a..ec3f6ddb6d9518be3302c8aa49ee07a2a0433310 100644 (file)
@@ -33,11 +33,73 @@ package jexer.bits;
  */
 public class CellAttributes {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Bold attribute.
      */
     private boolean bold;
 
+    /**
+     * Blink attribute.
+     */
+    private boolean blink;
+
+    /**
+     * Reverse attribute.
+     */
+    private boolean reverse;
+
+    /**
+     * Underline attribute.
+     */
+    private boolean underline;
+
+    /**
+     * Protected attribute.
+     */
+    private boolean protect;
+
+    /**
+     * Foreground color.  Color.WHITE, Color.RED, etc.
+     */
+    private Color foreColor;
+
+    /**
+     * Background color.  Color.WHITE, Color.RED, etc.
+     */
+    private Color backColor;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor sets default values of the cell to white-on-black,
+     * no bold/blink/reverse/underline/protect.
+     *
+     * @see #reset()
+     */
+    public CellAttributes() {
+        reset();
+    }
+
+    /**
+     * Public constructor makes a copy from another instance.
+     *
+     * @param that another CellAttributes instance
+     * @see #reset()
+     */
+    public CellAttributes(final CellAttributes that) {
+        setTo(that);
+    }
+
+    // ------------------------------------------------------------------------
+    // CellAttributes ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Getter for bold.
      *
@@ -56,11 +118,6 @@ public class CellAttributes {
         this.bold = bold;
     }
 
-    /**
-     * Blink attribute.
-     */
-    private boolean blink;
-
     /**
      * Getter for blink.
      *
@@ -79,11 +136,6 @@ public class CellAttributes {
         this.blink = blink;
     }
 
-    /**
-     * Reverse attribute.
-     */
-    private boolean reverse;
-
     /**
      * Getter for reverse.
      *
@@ -102,11 +154,6 @@ public class CellAttributes {
         this.reverse = reverse;
     }
 
-    /**
-     * Underline attribute.
-     */
-    private boolean underline;
-
     /**
      * Getter for underline.
      *
@@ -125,11 +172,6 @@ public class CellAttributes {
         this.underline = underline;
     }
 
-    /**
-     * Protected attribute.
-     */
-    private boolean protect;
-
     /**
      * Getter for protect.
      *
@@ -148,11 +190,6 @@ public class CellAttributes {
         this.protect = protect;
     }
 
-    /**
-     * Foreground color.  Color.WHITE, Color.RED, etc.
-     */
-    private Color foreColor;
-
     /**
      * Getter for foreColor.
      *
@@ -171,11 +208,6 @@ public class CellAttributes {
         this.foreColor = foreColor;
     }
 
-    /**
-     * Background color.  Color.WHITE, Color.RED, etc.
-     */
-    private Color backColor;
-
     /**
      * Getter for backColor.
      *
@@ -208,26 +240,6 @@ public class CellAttributes {
         backColor = Color.BLACK;
     }
 
-    /**
-     * Public constructor sets default values of the cell to white-on-black,
-     * no bold/blink/reverse/underline/protect.
-     *
-     * @see #reset()
-     */
-    public CellAttributes() {
-        reset();
-    }
-
-    /**
-     * Public constructor makes a copy from another instance.
-     *
-     * @param that another CellAttributes instance
-     * @see #reset()
-     */
-    public CellAttributes(final CellAttributes that) {
-        setTo(that);
-    }
-
     /**
      * Comparison check.  All fields must match to return true.
      *
index 0f4e6b32bf35229f3d018084eee52287f1dd5c52..120ff64a1c75f3dc7aeaf9da81aeb716c8787ff2 100644 (file)
@@ -33,62 +33,9 @@ package jexer.bits;
  */
 public final class Color {
 
-    /**
-     * The color value.  Default is SGRWHITE.
-     */
-    private int value = SGRWHITE;
-
-    /**
-     * Get color value.  Note that these deliberately match the color values
-     * of the ECMA-48 / ANSI X3.64 / VT100-ish SGR function ("ANSI colors").
-     *
-     * @return the value
-     */
-    public int getValue() {
-        return value;
-    }
-
-    /**
-     * Private constructor used to make the static Color instances.
-     *
-     * @param value the integer Color value
-     */
-    private Color(final int value) {
-        this.value = value;
-    }
-
-    /**
-     * Public constructor returns one of the static Color instances.
-     *
-     * @param colorName "red", "blue", etc.
-     * @return Color.RED, Color.BLUE, etc.
-     */
-    static Color getColor(final String colorName) {
-        String str = colorName.toLowerCase();
-
-        if (str.equals("black")) {
-            return Color.BLACK;
-        } else if (str.equals("white")) {
-            return Color.WHITE;
-        } else if (str.equals("red")) {
-            return Color.RED;
-        } else if (str.equals("cyan")) {
-            return Color.CYAN;
-        } else if (str.equals("green")) {
-            return Color.GREEN;
-        } else if (str.equals("magenta")) {
-            return Color.MAGENTA;
-        } else if (str.equals("blue")) {
-            return Color.BLUE;
-        } else if (str.equals("yellow")) {
-            return Color.YELLOW;
-        } else if (str.equals("brown")) {
-            return Color.YELLOW;
-        } else {
-            // Let unknown strings become white
-            return Color.WHITE;
-        }
-    }
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * SGR black value = 0.
@@ -170,6 +117,75 @@ public final class Color {
      */
     public static final Color WHITE = new Color(SGRWHITE);
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The color value.  Default is SGRWHITE.
+     */
+    private int value = SGRWHITE;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Private constructor used to make the static Color instances.
+     *
+     * @param value the integer Color value
+     */
+    private Color(final int value) {
+        this.value = value;
+    }
+
+    // ------------------------------------------------------------------------
+    // Color ------------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get color value.  Note that these deliberately match the color values
+     * of the ECMA-48 / ANSI X3.64 / VT100-ish SGR function ("ANSI colors").
+     *
+     * @return the value
+     */
+    public int getValue() {
+        return value;
+    }
+
+    /**
+     * Public constructor returns one of the static Color instances.
+     *
+     * @param colorName "red", "blue", etc.
+     * @return Color.RED, Color.BLUE, etc.
+     */
+    static Color getColor(final String colorName) {
+        String str = colorName.toLowerCase();
+
+        if (str.equals("black")) {
+            return Color.BLACK;
+        } else if (str.equals("white")) {
+            return Color.WHITE;
+        } else if (str.equals("red")) {
+            return Color.RED;
+        } else if (str.equals("cyan")) {
+            return Color.CYAN;
+        } else if (str.equals("green")) {
+            return Color.GREEN;
+        } else if (str.equals("magenta")) {
+            return Color.MAGENTA;
+        } else if (str.equals("blue")) {
+            return Color.BLUE;
+        } else if (str.equals("yellow")) {
+            return Color.YELLOW;
+        } else if (str.equals("brown")) {
+            return Color.YELLOW;
+        } else {
+            // Let unknown strings become white
+            return Color.WHITE;
+        }
+    }
+
     /**
      * Invert a color in the same way as (CGA/VGA color XOR 0x7).
      *
index baf7685b4ef2331a2cb07a41e67700bc745a4c00..0c6f6e4a1ea35eccb6c1906948fa41ef6be75640 100644 (file)
@@ -46,11 +46,19 @@ import java.util.TreeMap;
  */
 public final class ColorTheme {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The current theme colors.
      */
     private SortedMap<String, CellAttributes> colors;
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Public constructor sets the theme to the default.
      */
@@ -59,6 +67,10 @@ public final class ColorTheme {
         setDefaultTheme();
     }
 
+    // ------------------------------------------------------------------------
+    // ColorTheme -------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Retrieve the CellAttributes for a named theme color.
      *
@@ -454,6 +466,11 @@ public final class ColorTheme {
         color.setBackColor(Color.BLUE);
         color.setBold(false);
         colors.put("ttreeview.inactive", color);
+        color = new CellAttributes();
+        color.setForeColor(Color.BLACK);
+        color.setBackColor(Color.WHITE);
+        color.setBold(false);
+        colors.put("ttreeview.selected.inactive", color);
 
         // TList
         color = new CellAttributes();
@@ -479,6 +496,11 @@ public final class ColorTheme {
         color.setBackColor(Color.BLUE);
         color.setBold(false);
         colors.put("tlist.inactive", color);
+        color = new CellAttributes();
+        color.setForeColor(Color.BLACK);
+        color.setBackColor(Color.WHITE);
+        color.setBold(false);
+        colors.put("tlist.selected.inactive", color);
 
         // TStatusBar
         color = new CellAttributes();
index 32403090a4301a0ce800b1dfe1fe3668cd335b20..b571639a0384d4e73fdbb8313da22e55c49f994b 100644 (file)
@@ -34,11 +34,9 @@ package jexer.bits;
  */
 public final class GraphicsChars {
 
-    /**
-     * Private constructor prevents accidental creation of this class.
-     */
-    private GraphicsChars() {
-    }
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * The CP437 to Unicode translation map.
@@ -147,4 +145,15 @@ public final class GraphicsChars {
     public static final char WINDOW_RIGHT_BOTTOM_DOUBLE = CP437[0xBC];
     public static final char VERTICAL_BAR               = CP437[0xB3];
     public static final char OCTOSTAR                   = CP437[0x0F];
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Private constructor prevents accidental creation of this class.
+     */
+    private GraphicsChars() {
+    }
+
 }
index 327987987a84b0d0410100a382f4f86f34ff4f79..edd5227b7136349be47211f3e814a16cbd70c87b 100644 (file)
@@ -36,47 +36,28 @@ package jexer.bits;
  */
 public final class MnemonicString {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Keyboard shortcut to activate this item.
      */
     private char shortcut;
 
-    /**
-     * Get the keyboard shortcut character.
-     *
-     * @return the highlighted character
-     */
-    public char getShortcut() {
-        return shortcut;
-    }
-
     /**
      * Location of the highlighted character.
      */
     private int shortcutIdx = -1;
 
-    /**
-     * Get location of the highlighted character.
-     *
-     * @return location of the highlighted character
-     */
-    public int getShortcutIdx() {
-        return shortcutIdx;
-    }
-
     /**
      * The raw (uncolored) string.
      */
     private String rawLabel;
 
-    /**
-     * Get the raw (uncolored) string.
-     *
-     * @return the raw (uncolored) string
-     */
-    public String getRawLabel() {
-        return rawLabel;
-    }
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Public constructor.
@@ -116,4 +97,36 @@ public final class MnemonicString {
         }
         this.rawLabel = newLabel;
     }
+
+    // ------------------------------------------------------------------------
+    // MnemonicString ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the keyboard shortcut character.
+     *
+     * @return the highlighted character
+     */
+    public char getShortcut() {
+        return shortcut;
+    }
+
+    /**
+     * Get location of the highlighted character.
+     *
+     * @return location of the highlighted character
+     */
+    public int getShortcutIdx() {
+        return shortcutIdx;
+    }
+
+    /**
+     * Get the raw (uncolored) string.
+     *
+     * @return the raw (uncolored) string
+     */
+    public String getRawLabel() {
+        return rawLabel;
+    }
+
 }
similarity index 85%
rename from src/jexer/bits/StringJustifier.java
rename to src/jexer/bits/StringUtils.java
index bbaa9cb18722f7104c6996c3160d577dce122025..535720da7f82d235eb52465500ab802e2c84f97c 100644 (file)
@@ -32,10 +32,15 @@ import java.util.List;
 import java.util.LinkedList;
 
 /**
- * StringJustifier contains methods to convert one or more long lines of
- * strings into justified text paragraphs.
+ * StringUtils contains methods to:
+ *
+ *    - Convert one or more long lines of strings into justified text
+ *      paragraphs.
+ *
+ *    - Unescape C0 control codes.
+ *
  */
-public final class StringJustifier {
+public final class StringUtils {
 
     /**
      * Left-justify a string into a list of lines.
@@ -237,4 +242,45 @@ public final class StringJustifier {
         return result;
     }
 
+    /**
+     * Convert raw strings into escaped strings that be splatted on the
+     * screen.
+     *
+     * @param str the string
+     * @return a string that can be passed into Screen.putStringXY()
+     */
+    public static String unescape(final String str) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < str.length(); i++) {
+            char ch = str.charAt(i);
+            if ((ch < 0x20) || (ch == 0x7F)) {
+                switch (ch) {
+                case '\b':
+                    sb.append("\\b");
+                    continue;
+                case '\f':
+                    sb.append("\\f");
+                    continue;
+                case '\n':
+                    sb.append("\\n");
+                    continue;
+                case '\r':
+                    sb.append("\\r");
+                    continue;
+                case '\t':
+                    sb.append("\\t");
+                    continue;
+                case 0x7f:
+                    sb.append("^?");
+                    continue;
+                default:
+                    sb.append(' ');
+                    continue;
+                }
+            }
+            sb.append(ch);
+        }
+        return sb.toString();
+    }
+
 }
index 7697981c015bb8b0db59874b0091ee3697ba8332..a4bcbe25ef04ee854d85f99bd4ed3f9a54dbd27e 100644 (file)
@@ -32,6 +32,7 @@ import java.io.IOException;
 
 import jexer.*;
 import jexer.event.*;
+import jexer.ttree.*;
 import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
@@ -43,7 +44,7 @@ public class DemoTreeViewWindow extends TWindow {
     /**
      * Hang onto my TTreeView so I can resize it with the window.
      */
-    private TTreeView treeView;
+    private TTreeViewWidget treeView;
 
     /**
      * Public constructor.
@@ -55,7 +56,7 @@ public class DemoTreeViewWindow extends TWindow {
         super(parent, "Tree View", 0, 0, 44, 16, TWindow.RESIZABLE);
 
         // Load the treeview with "stuff"
-        treeView = addTreeView(1, 1, 40, 12);
+        treeView = addTreeViewWidget(1, 1, 40, 12);
         new TDirectoryTreeItem(treeView, ".", true);
 
         statusBar = newStatusBar("Treeview demonstration");
index e4b44833fe4a0893e58de6c53df28858069a6ca8..a6a0df2a98a90a7c0ca5f42e706f784de72ca6d0 100644 (file)
@@ -35,6 +35,10 @@ package jexer.event;
  */
 public final class TMouseEvent extends TInputEvent {
 
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The type of event generated.
      */
@@ -60,11 +64,99 @@ public final class TMouseEvent extends TInputEvent {
         MOUSE_DOUBLE_CLICK
     }
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Type of event, one of MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN.
      */
     private Type type;
 
+    /**
+     * Mouse X - relative coordinates.
+     */
+    private int x;
+
+    /**
+     * Mouse Y - relative coordinates.
+     */
+    private int y;
+
+    /**
+     * Mouse X - absolute screen coordinates.
+     */
+    private int absoluteX;
+
+    /**
+     * Mouse Y - absolute screen coordinate.
+     */
+    private int absoluteY;
+
+    /**
+     * Mouse button 1 (left button).
+     */
+    private boolean mouse1;
+
+    /**
+     * Mouse button 2 (right button).
+     */
+    private boolean mouse2;
+
+    /**
+     * Mouse button 3 (middle button).
+     */
+    private boolean mouse3;
+
+    /**
+     * Mouse wheel UP (button 4).
+     */
+    private boolean mouseWheelUp;
+
+    /**
+     * Mouse wheel DOWN (button 5).
+     */
+    private boolean mouseWheelDown;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public contructor.
+     *
+     * @param type the type of event, MOUSE_MOTION, MOUSE_DOWN, or MOUSE_UP
+     * @param x relative column
+     * @param y relative row
+     * @param absoluteX absolute column
+     * @param absoluteY absolute row
+     * @param mouse1 if true, left button is down
+     * @param mouse2 if true, right button is down
+     * @param mouse3 if true, middle button is down
+     * @param mouseWheelUp if true, mouse wheel (button 4) is down
+     * @param mouseWheelDown if true, mouse wheel (button 5) is down
+     */
+    public TMouseEvent(final Type type, final int x, final int y,
+        final int absoluteX, final int absoluteY,
+        final boolean mouse1, final boolean mouse2, final boolean mouse3,
+        final boolean mouseWheelUp, final boolean mouseWheelDown) {
+
+        this.type               = type;
+        this.x                  = x;
+        this.y                  = y;
+        this.absoluteX          = absoluteX;
+        this.absoluteY          = absoluteY;
+        this.mouse1             = mouse1;
+        this.mouse2             = mouse2;
+        this.mouse3             = mouse3;
+        this.mouseWheelUp       = mouseWheelUp;
+        this.mouseWheelDown     = mouseWheelDown;
+    }
+
+    // ------------------------------------------------------------------------
+    // TMouseEvent ------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Get type.
      *
@@ -74,11 +166,6 @@ public final class TMouseEvent extends TInputEvent {
         return type;
     }
 
-    /**
-     * Mouse X - relative coordinates.
-     */
-    private int x;
-
     /**
      * Get x.
      *
@@ -100,11 +187,6 @@ public final class TMouseEvent extends TInputEvent {
         this.x = x;
     }
 
-    /**
-     * Mouse Y - relative coordinates.
-     */
-    private int y;
-
     /**
      * Get y.
      *
@@ -126,11 +208,6 @@ public final class TMouseEvent extends TInputEvent {
         this.y = y;
     }
 
-    /**
-     * Mouse X - absolute screen coordinates.
-     */
-    private int absoluteX;
-
     /**
      * Get absoluteX.
      *
@@ -149,11 +226,6 @@ public final class TMouseEvent extends TInputEvent {
         this.absoluteX = absoluteX;
     }
 
-    /**
-     * Mouse Y - absolute screen coordinate.
-     */
-    private int absoluteY;
-
     /**
      * Get absoluteY.
      *
@@ -172,11 +244,6 @@ public final class TMouseEvent extends TInputEvent {
         this.absoluteY = absoluteY;
     }
 
-    /**
-     * Mouse button 1 (left button).
-     */
-    private boolean mouse1;
-
     /**
      * Get mouse1.
      *
@@ -186,11 +253,6 @@ public final class TMouseEvent extends TInputEvent {
         return mouse1;
     }
 
-    /**
-     * Mouse button 2 (right button).
-     */
-    private boolean mouse2;
-
     /**
      * Get mouse2.
      *
@@ -200,11 +262,6 @@ public final class TMouseEvent extends TInputEvent {
         return mouse2;
     }
 
-    /**
-     * Mouse button 3 (middle button).
-     */
-    private boolean mouse3;
-
     /**
      * Get mouse3.
      *
@@ -214,11 +271,6 @@ public final class TMouseEvent extends TInputEvent {
         return mouse3;
     }
 
-    /**
-     * Mouse wheel UP (button 4).
-     */
-    private boolean mouseWheelUp;
-
     /**
      * Get mouseWheelUp.
      *
@@ -228,11 +280,6 @@ public final class TMouseEvent extends TInputEvent {
         return mouseWheelUp;
     }
 
-    /**
-     * Mouse wheel DOWN (button 5).
-     */
-    private boolean mouseWheelDown;
-
     /**
      * Get mouseWheelDown.
      *
@@ -242,37 +289,6 @@ public final class TMouseEvent extends TInputEvent {
         return mouseWheelDown;
     }
 
-    /**
-     * Public contructor.
-     *
-     * @param type the type of event, MOUSE_MOTION, MOUSE_DOWN, or MOUSE_UP
-     * @param x relative column
-     * @param y relative row
-     * @param absoluteX absolute column
-     * @param absoluteY absolute row
-     * @param mouse1 if true, left button is down
-     * @param mouse2 if true, right button is down
-     * @param mouse3 if true, middle button is down
-     * @param mouseWheelUp if true, mouse wheel (button 4) is down
-     * @param mouseWheelDown if true, mouse wheel (button 5) is down
-     */
-    public TMouseEvent(final Type type, final int x, final int y,
-        final int absoluteX, final int absoluteY,
-        final boolean mouse1, final boolean mouse2, final boolean mouse3,
-        final boolean mouseWheelUp, final boolean mouseWheelDown) {
-
-        this.type               = type;
-        this.x                  = x;
-        this.y                  = y;
-        this.absoluteX          = absoluteX;
-        this.absoluteY          = absoluteY;
-        this.mouse1             = mouse1;
-        this.mouse2             = mouse2;
-        this.mouse3             = mouse3;
-        this.mouseWheelUp       = mouseWheelUp;
-        this.mouseWheelDown     = mouseWheelDown;
-    }
-
     /**
      * Create a duplicate instance.
      *
index 8b2b36759c414c3c564f7cbb30b4d5f89c689d50..4e15121c3fddf49e58e79d9818d63e365fa290bd 100644 (file)
@@ -33,6 +33,10 @@ package jexer.event;
  */
 public final class TResizeEvent extends TInputEvent {
 
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Resize events can be generated for either a total screen resize or a
      * widget/window resize.
@@ -49,11 +53,46 @@ public final class TResizeEvent extends TInputEvent {
         WIDGET
     }
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The type of resize.
      */
     private Type type;
 
+    /**
+     * New width.
+     */
+    private int width;
+
+    /**
+     * New height.
+     */
+    private int height;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public contructor.
+     *
+     * @param type the Type of resize, Screen or Widget
+     * @param width the new width
+     * @param height the new height
+     */
+    public TResizeEvent(final Type type, final int width, final int height) {
+        this.type   = type;
+        this.width  = width;
+        this.height = height;
+    }
+
+    // ------------------------------------------------------------------------
+    // TResizeEvent -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Get resize type.
      *
@@ -63,11 +102,6 @@ public final class TResizeEvent extends TInputEvent {
         return type;
     }
 
-    /**
-     * New width.
-     */
-    private int width;
-
     /**
      * Get the new width.
      *
@@ -77,11 +111,6 @@ public final class TResizeEvent extends TInputEvent {
         return width;
     }
 
-    /**
-     * New height.
-     */
-    private int height;
-
     /**
      * Get the new height.
      *
@@ -91,19 +120,6 @@ public final class TResizeEvent extends TInputEvent {
         return height;
     }
 
-    /**
-     * Public contructor.
-     *
-     * @param type the Type of resize, Screen or Widget
-     * @param width the new width
-     * @param height the new height
-     */
-    public TResizeEvent(final Type type, final int width, final int height) {
-        this.type   = type;
-        this.width  = width;
-        this.height = height;
-    }
-
     /**
      * Make human-readable description of this TResizeEvent.
      *
index 667673b642c4f340dd91b411fe38e3904d0f5687..be4cf5dd12646bd107d7841a3dc49edd15996cd9 100644 (file)
@@ -51,47 +51,9 @@ public final class TMenu extends TWindow {
      */
     private static final ResourceBundle i18n = ResourceBundle.getBundle(TMenu.class.getName());
 
-    /**
-     * If true, this is a sub-menu.  Note package private access.
-     */
-    boolean isSubMenu = false;
-
-    /**
-     * The X position of the menu's title.
-     */
-    private int titleX;
-
-    /**
-     * Set the menu title X position.
-     *
-     * @param titleX the position
-     */
-    public void setTitleX(final int titleX) {
-        this.titleX = titleX;
-    }
-
-    /**
-     * Get the menu title X position.
-     *
-     * @return the position
-     */
-    public int getTitleX() {
-        return titleX;
-    }
-
-    /**
-     * The shortcut and title.
-     */
-    private MnemonicString mnemonic;
-
-    /**
-     * Get the mnemonic string.
-     *
-     * @return the full mnemonic string
-     */
-    public MnemonicString getMnemonic() {
-        return mnemonic;
-    }
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     // Reserved menu item IDs
     public static final int MID_UNUSED          = -1;
@@ -108,15 +70,21 @@ public final class TMenu extends TWindow {
     public static final int MID_PASTE           = 12;
     public static final int MID_CLEAR           = 13;
 
+    // Search menu
+    public static final int MID_FIND            = 20;
+    public static final int MID_REPLACE         = 21;
+    public static final int MID_SEARCH_AGAIN    = 22;
+    public static final int MID_GOTO_LINE       = 23;
+
     // Window menu
-    public static final int MID_TILE            = 20;
-    public static final int MID_CASCADE         = 21;
-    public static final int MID_CLOSE_ALL       = 22;
-    public static final int MID_WINDOW_MOVE     = 23;
-    public static final int MID_WINDOW_ZOOM     = 24;
-    public static final int MID_WINDOW_NEXT     = 25;
-    public static final int MID_WINDOW_PREVIOUS = 26;
-    public static final int MID_WINDOW_CLOSE    = 27;
+    public static final int MID_TILE            = 30;
+    public static final int MID_CASCADE         = 31;
+    public static final int MID_CLOSE_ALL       = 32;
+    public static final int MID_WINDOW_MOVE     = 33;
+    public static final int MID_WINDOW_ZOOM     = 34;
+    public static final int MID_WINDOW_NEXT     = 35;
+    public static final int MID_WINDOW_PREVIOUS = 36;
+    public static final int MID_WINDOW_CLOSE    = 37;
 
     // Help menu
     public static final int MID_HELP_CONTENTS           = 40;
@@ -130,6 +98,29 @@ public final class TMenu extends TWindow {
     // Other
     public static final int MID_REPAINT         = 50;
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * If true, this is a sub-menu.  Note package private access.
+     */
+    boolean isSubMenu = false;
+
+    /**
+     * The X position of the menu's title.
+     */
+    private int titleX;
+
+    /**
+     * The shortcut and title.
+     */
+    private MnemonicString mnemonic;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Public constructor.
      *
@@ -158,46 +149,9 @@ public final class TMenu extends TWindow {
         setActive(false);
     }
 
-    /**
-     * Draw a top-level menu with title and menu items.
-     */
-    @Override
-    public void draw() {
-        CellAttributes background = getTheme().getColor("tmenu");
-
-        assert (isAbsoluteActive());
-
-        // Fill in the interior background
-        for (int i = 0; i < getHeight(); i++) {
-            hLineXY(0, i, getWidth(), ' ', background);
-        }
-
-        // Draw the box
-        char cTopLeft;
-        char cTopRight;
-        char cBottomLeft;
-        char cBottomRight;
-        char cHSide;
-
-        cTopLeft = GraphicsChars.ULCORNER;
-        cTopRight = GraphicsChars.URCORNER;
-        cBottomLeft = GraphicsChars.LLCORNER;
-        cBottomRight = GraphicsChars.LRCORNER;
-        cHSide = GraphicsChars.SINGLE_BAR;
-
-        // Place the corner characters
-        putCharXY(1, 0, cTopLeft, background);
-        putCharXY(getWidth() - 2, 0, cTopRight, background);
-        putCharXY(1, getHeight() - 1, cBottomLeft, background);
-        putCharXY(getWidth() - 2, getHeight() - 1, cBottomRight, background);
-
-        // Draw the box lines
-        hLineXY(1 + 1, 0, getWidth() - 4, cHSide, background);
-        hLineXY(1 + 1, getHeight() - 1, getWidth() - 4, cHSide, background);
-
-        // Draw a shadow
-        getScreen().drawBoxShadow(0, 0, getWidth(), getHeight());
-    }
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Handle mouse button presses.
@@ -345,6 +299,82 @@ public final class TMenu extends TWindow {
         }
     }
 
+    // ------------------------------------------------------------------------
+    // TWindow ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw a top-level menu with title and menu items.
+     */
+    @Override
+    public void draw() {
+        CellAttributes background = getTheme().getColor("tmenu");
+
+        assert (isAbsoluteActive());
+
+        // Fill in the interior background
+        for (int i = 0; i < getHeight(); i++) {
+            hLineXY(0, i, getWidth(), ' ', background);
+        }
+
+        // Draw the box
+        char cTopLeft;
+        char cTopRight;
+        char cBottomLeft;
+        char cBottomRight;
+        char cHSide;
+
+        cTopLeft = GraphicsChars.ULCORNER;
+        cTopRight = GraphicsChars.URCORNER;
+        cBottomLeft = GraphicsChars.LLCORNER;
+        cBottomRight = GraphicsChars.LRCORNER;
+        cHSide = GraphicsChars.SINGLE_BAR;
+
+        // Place the corner characters
+        putCharXY(1, 0, cTopLeft, background);
+        putCharXY(getWidth() - 2, 0, cTopRight, background);
+        putCharXY(1, getHeight() - 1, cBottomLeft, background);
+        putCharXY(getWidth() - 2, getHeight() - 1, cBottomRight, background);
+
+        // Draw the box lines
+        hLineXY(1 + 1, 0, getWidth() - 4, cHSide, background);
+        hLineXY(1 + 1, getHeight() - 1, getWidth() - 4, cHSide, background);
+
+        // Draw a shadow
+        getScreen().drawBoxShadow(0, 0, getWidth(), getHeight());
+    }
+
+    // ------------------------------------------------------------------------
+    // TMenu ------------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Set the menu title X position.
+     *
+     * @param titleX the position
+     */
+    public void setTitleX(final int titleX) {
+        this.titleX = titleX;
+    }
+
+    /**
+     * Get the menu title X position.
+     *
+     * @return the position
+     */
+    public int getTitleX() {
+        return titleX;
+    }
+
+    /**
+     * Get the mnemonic string.
+     *
+     * @return the full mnemonic string
+     */
+    public MnemonicString getMnemonic() {
+        return mnemonic;
+    }
+
     /**
      * Convenience function to add a menu item.
      *
@@ -400,11 +430,27 @@ public final class TMenu extends TWindow {
     private TMenuItem addItemInternal(final int id, final String label,
         final TKeypress key) {
 
+        return addItemInternal(id, label, key, true);
+    }
+
+    /**
+     * Convenience function to add a custom menu item.
+     *
+     * @param id menu item ID.  Must be greater than 1024.
+     * @param label menu item label
+     * @param key global keyboard accelerator
+     * @param enabled default state for enabled
+     * @return the new menu item
+     */
+    private TMenuItem addItemInternal(final int id, final String label,
+        final TKeypress key, final boolean enabled) {
+
         int newY = getChildren().size() + 1;
         assert (newY < getHeight());
 
         TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
         menuItem.setKey(key);
+        menuItem.setEnabled(enabled);
         setHeight(getHeight() + 1);
         if (menuItem.getWidth() + 2 > getWidth()) {
             setWidth(menuItem.getWidth() + 2);
@@ -426,6 +472,18 @@ public final class TMenu extends TWindow {
      * @return the new menu item
      */
     public TMenuItem addDefaultItem(final int id) {
+        return addDefaultItem(id, true);
+    }
+
+    /**
+     * Convenience function to add one of the default menu items.
+     *
+     * @param id menu item ID.  Must be between 0 (inclusive) and 1023
+     * (inclusive).
+     * @param enabled default state for enabled
+     * @return the new menu item
+     */
+    public TMenuItem addDefaultItem(final int id, final boolean enabled) {
         assert (id >= 0);
         assert (id < 1024);
 
@@ -465,6 +523,20 @@ public final class TMenu extends TWindow {
             // key = kbDel;
             break;
 
+        case MID_FIND:
+            label = i18n.getString("menuFind");
+            break;
+        case MID_REPLACE:
+            label = i18n.getString("menuReplace");
+            break;
+        case MID_SEARCH_AGAIN:
+            label = i18n.getString("menuSearchAgain");
+            break;
+        case MID_GOTO_LINE:
+            label = i18n.getString("menuGotoLine");
+            key = kbCtrlL;
+            break;
+
         case MID_TILE:
             label = i18n.getString("menuWindowTile");
             break;
@@ -528,7 +600,7 @@ public final class TMenu extends TWindow {
             throw new IllegalArgumentException("Invalid menu ID: " + id);
         }
 
-        return addItemInternal(id, label, key);
+        return addItemInternal(id, label, key, enabled);
     }
 
     /**
index 000df917d26052a345b5d7ad9e4973114e350a71..d42157cb04a498d2d6c408a9657cf32342379bb4 100644 (file)
@@ -5,6 +5,10 @@ menuCut=Cu&t
 menuCopy=&Copy
 menuPaste=&Paste
 menuClear=C&lear
+menuFind=&Find...
+menuReplace=&Replace...
+menuSearchAgain=&Search again
+menuGotoLine=&Go to line number...
 menuWindowTile=&Tile
 menuWindowCascade=C&ascade
 menuWindowCloseAll=Cl&ose All
index 9ffc1ea075a6448c561e21ef5008f87961f7f4ff..63a5355e348f0e553215a67c88d45c9979a856d5 100644 (file)
@@ -43,6 +43,10 @@ import static jexer.TKeypress.*;
  */
 public class TMenuItem extends TWidget {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Label for this menu item.
      */
@@ -54,29 +58,11 @@ public class TMenuItem extends TWidget {
      */
     private int id = TMenu.MID_UNUSED;
 
-    /**
-     * Get the menu item ID.
-     *
-     * @return the id
-     */
-    public final int getId() {
-        return id;
-    }
-
     /**
      * When true, this item can be checked or unchecked.
      */
     private boolean checkable = false;
 
-    /**
-     * Set checkable flag.
-     *
-     * @param checkable if true, this menu item can be checked/unchecked
-     */
-    public final void setCheckable(final boolean checkable) {
-        this.checkable = checkable;
-    }
-
     /**
      * When true, this item is checked.
      */
@@ -93,40 +79,9 @@ public class TMenuItem extends TWidget {
      */
     private MnemonicString mnemonic;
 
-    /**
-     * Get the mnemonic string for this menu item.
-     *
-     * @return mnemonic string
-     */
-    public final MnemonicString getMnemonic() {
-        return mnemonic;
-    }
-
-    /**
-     * Get a global accelerator key for this menu item.
-     *
-     * @return global keyboard accelerator, or null if no key is associated
-     * with this item
-     */
-    public final TKeypress getKey() {
-        return key;
-    }
-
-    /**
-     * Set a global accelerator key for this menu item.
-     *
-     * @param key global keyboard accelerator
-     */
-    public final void setKey(final TKeypress key) {
-        this.key = key;
-
-        if (key != null) {
-            int newWidth = (label.length() + 4 + key.toString().length() + 2);
-            if (newWidth > getWidth()) {
-                setWidth(newWidth);
-            }
-        }
-    }
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Package private constructor.
@@ -190,6 +145,10 @@ public class TMenuItem extends TWidget {
 
     }
 
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Returns true if the mouse is currently on the menu item.
      *
@@ -206,6 +165,39 @@ public class TMenuItem extends TWidget {
         return false;
     }
 
+    /**
+     * Handle mouse button releases.
+     *
+     * @param mouse mouse button release event
+     */
+    @Override
+    public void onMouseUp(final TMouseEvent mouse) {
+        if ((mouseOnMenuItem(mouse)) && (mouse.isMouse1())) {
+            dispatch();
+            return;
+        }
+    }
+
+    /**
+     * Handle keystrokes.
+     *
+     * @param keypress keystroke event
+     */
+    @Override
+    public void onKeypress(final TKeypressEvent keypress) {
+        if (keypress.equals(kbEnter)) {
+            dispatch();
+            return;
+        }
+
+        // Pass to parent for the things we don't care about.
+        super.onKeypress(keypress);
+    }
+
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Draw a menu item with label.
      */
@@ -249,44 +241,73 @@ public class TMenuItem extends TWidget {
 
     }
 
+    // ------------------------------------------------------------------------
+    // TMenuItem --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
-     * Dispatch event(s) due to selection or click.
+     * Get the menu item ID.
+     *
+     * @return the id
      */
-    public void dispatch() {
-        assert (isEnabled());
+    public final int getId() {
+        return id;
+    }
 
-        getApplication().postMenuEvent(new TMenuEvent(id));
-        if (checkable) {
-            checked = !checked;
-        }
+    /**
+     * Set checkable flag.
+     *
+     * @param checkable if true, this menu item can be checked/unchecked
+     */
+    public final void setCheckable(final boolean checkable) {
+        this.checkable = checkable;
     }
 
     /**
-     * Handle mouse button releases.
+     * Get the mnemonic string for this menu item.
      *
-     * @param mouse mouse button release event
+     * @return mnemonic string
      */
-    @Override
-    public void onMouseUp(final TMouseEvent mouse) {
-        if ((mouseOnMenuItem(mouse)) && (mouse.isMouse1())) {
-            dispatch();
-            return;
-        }
+    public final MnemonicString getMnemonic() {
+        return mnemonic;
     }
 
     /**
-     * Handle keystrokes.
+     * Get a global accelerator key for this menu item.
      *
-     * @param keypress keystroke event
+     * @return global keyboard accelerator, or null if no key is associated
+     * with this item
      */
-    @Override
-    public void onKeypress(final TKeypressEvent keypress) {
-        if (keypress.equals(kbEnter)) {
-            dispatch();
-            return;
+    public final TKeypress getKey() {
+        return key;
+    }
+
+    /**
+     * Set a global accelerator key for this menu item.
+     *
+     * @param key global keyboard accelerator
+     */
+    public final void setKey(final TKeypress key) {
+        this.key = key;
+
+        if (key != null) {
+            int newWidth = (label.length() + 4 + key.toString().length() + 2);
+            if (newWidth > getWidth()) {
+                setWidth(newWidth);
+            }
         }
+    }
 
-        // Pass to parent for the things we don't care about.
-        super.onKeypress(keypress);
+    /**
+     * Dispatch event(s) due to selection or click.
+     */
+    public void dispatch() {
+        assert (isEnabled());
+
+        getApplication().postMenuEvent(new TMenuEvent(id));
+        if (checkable) {
+            checked = !checked;
+        }
     }
+
 }
index 9a9fcfc81a0e39143d64481aeba57b3c0d9cc009..01164a537a63aed47d01e5d8356f101ce4107bee 100644 (file)
@@ -36,6 +36,10 @@ import jexer.bits.GraphicsChars;
  */
 public final class TMenuSeparator extends TMenuItem {
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Package private constructor.
      *
@@ -50,6 +54,10 @@ public final class TMenuSeparator extends TMenuItem {
         setWidth(parent.getWidth() - 2);
     }
 
+    // ------------------------------------------------------------------------
+    // TMenuItem --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Draw a menu separator.
      */
index 7b5f80c911e6e402dbf69dd9dc92c0e1a23b5b35..88094daa155b0ba46e9f758084510a352c8c0415 100644 (file)
@@ -40,11 +40,19 @@ import static jexer.TKeypress.*;
  */
 public final class TSubMenu extends TMenuItem {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The menu window.  Note package private access.
      */
     TMenu menu;
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Package private constructor.
      *
@@ -67,28 +75,9 @@ public final class TSubMenu extends TMenuItem {
         this.menu.isSubMenu = true;
     }
 
-    /**
-     * Draw the menu title.
-     */
-    @Override
-    public void draw() {
-        super.draw();
-
-        CellAttributes menuColor;
-        if (isAbsoluteActive()) {
-            menuColor = getTheme().getColor("tmenu.highlighted");
-        } else {
-            if (isEnabled()) {
-                menuColor = getTheme().getColor("tmenu");
-            } else {
-                menuColor = getTheme().getColor("tmenu.disabled");
-            }
-        }
-
-        // Add the arrow
-        getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.CP437[0x10],
-            menuColor);
-    }
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Handle keystrokes.
@@ -151,6 +140,33 @@ public final class TSubMenu extends TMenuItem {
         }
     }
 
+    // ------------------------------------------------------------------------
+    // TMenuItem --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw the menu title.
+     */
+    @Override
+    public void draw() {
+        super.draw();
+
+        CellAttributes menuColor;
+        if (isAbsoluteActive()) {
+            menuColor = getTheme().getColor("tmenu.highlighted");
+        } else {
+            if (isEnabled()) {
+                menuColor = getTheme().getColor("tmenu");
+            } else {
+                menuColor = getTheme().getColor("tmenu.disabled");
+            }
+        }
+
+        // Add the arrow
+        getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.CP437[0x10],
+            menuColor);
+    }
+
     /**
      * Override dispatch() to do nothing.
      */
@@ -179,6 +195,10 @@ public final class TSubMenu extends TMenuItem {
         return this;
     }
 
+    // ------------------------------------------------------------------------
+    // TSubMenu ---------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Convenience function to add a custom menu item.
      *
@@ -233,5 +253,4 @@ public final class TSubMenu extends TMenuItem {
         return menu.addSubMenu(title);
     }
 
-
 }
index ea30171c30d38a47a4acbf13e035739db6a48424..9ae77476de7d5d39305ce356149d58494fd6e2d3 100644 (file)
@@ -43,6 +43,14 @@ import static jexer.net.TelnetSocket.*;
 public final class TelnetInputStream extends InputStream
         implements SessionInfo {
 
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The root TelnetSocket that has my telnet protocol state.
      */
@@ -76,6 +84,61 @@ public final class TelnetInputStream extends InputStream
      */
     private int readBufferStart;
 
+    /**
+     * User name.
+     */
+    private String username = "";
+
+    /**
+     * Language.
+     */
+    private String language = "en_US";
+
+    /**
+     * Text window width.
+     */
+    private int windowWidth = 80;
+
+    /**
+     * Text window height.
+     */
+    private int windowHeight = 24;
+
+    /**
+     * When true, the last read byte from the remote side was IAC.
+     */
+    private boolean iac = false;
+
+    /**
+     * When true, we are in the middle of a DO/DONT/WILL/WONT negotiation.
+     */
+    private boolean dowill = false;
+
+    /**
+     * The telnet option being negotiated.
+     */
+    private int dowillType = 0;
+
+    /**
+     * When true, we are waiting to see the end of the sub-negotiation
+     * sequence.
+     */
+    private boolean subnegEnd = false;
+
+    /**
+     * When true, the last byte read from the remote side was CR.
+     */
+    private boolean readCR = false;
+
+    /**
+     * The subnegotiation buffer.
+     */
+    private ArrayList<Byte> subnegBuffer;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Package private constructor.
      *
@@ -97,27 +160,9 @@ public final class TelnetInputStream extends InputStream
         subnegBuffer    = new ArrayList<Byte>();
     }
 
-    // SessionInfo interface --------------------------------------------------
-
-    /**
-     * User name.
-     */
-    private String username = "";
-
-    /**
-     * Language.
-     */
-    private String language = "en_US";
-
-    /**
-     * Text window width.
-     */
-    private int windowWidth = 80;
-
-    /**
-     * Text window height.
-     */
-    private int windowHeight = 24;
+    // ------------------------------------------------------------------------
+    // SessionInfo ------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Username getter.
@@ -180,7 +225,9 @@ public final class TelnetInputStream extends InputStream
         // NOP
     }
 
-    // InputStream interface --------------------------------------------------
+    // ------------------------------------------------------------------------
+    // InputStream ------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Returns an estimate of the number of bytes that can be read (or
@@ -371,39 +418,9 @@ public final class TelnetInputStream extends InputStream
         return n;
     }
 
-    // Telnet protocol --------------------------------------------------------
-
-
-    /**
-     * When true, the last read byte from the remote side was IAC.
-     */
-    private boolean iac = false;
-
-    /**
-     * When true, we are in the middle of a DO/DONT/WILL/WONT negotiation.
-     */
-    private boolean dowill = false;
-
-    /**
-     * The telnet option being negotiated.
-     */
-    private int dowillType = 0;
-
-    /**
-     * When true, we are waiting to see the end of the sub-negotiation
-     * sequence.
-     */
-    private boolean subnegEnd = false;
-
-    /**
-     * When true, the last byte read from the remote side was CR.
-     */
-    private boolean readCR = false;
-
-    /**
-     * The subnegotiation buffer.
-     */
-    private ArrayList<Byte> subnegBuffer;
+    // ------------------------------------------------------------------------
+    // TelnetInputStream ------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * For debugging, return a descriptive string for this telnet option.
@@ -1348,5 +1365,4 @@ public final class TelnetInputStream extends InputStream
         return bufN;
     }
 
-
 }
index 3520a591f63e4dbc3e19988ac038eddd59eed425..338de2c24f4115198445d5c709a246a90aa29978 100644 (file)
@@ -38,6 +38,10 @@ import static jexer.net.TelnetSocket.*;
  */
 public final class TelnetOutputStream extends OutputStream {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The root TelnetSocket that has my telnet protocol state.
      */
@@ -48,6 +52,15 @@ public final class TelnetOutputStream extends OutputStream {
      */
     private OutputStream output;
 
+    /**
+     * When true, the last byte the caller passed to write() was a CR.
+     */
+    private boolean writeCR = false;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Package private constructor.
      *
@@ -59,7 +72,9 @@ public final class TelnetOutputStream extends OutputStream {
         this.output = output;
     }
 
-    // OutputStream interface -------------------------------------------------
+    // ------------------------------------------------------------------------
+    // OutputStrem ------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Closes this output stream and releases any system resources associated
@@ -135,6 +150,10 @@ public final class TelnetOutputStream extends OutputStream {
         writeImpl(bytes, 0, 1);
     }
 
+    // ------------------------------------------------------------------------
+    // TelnetOutputStrem ------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Writes b.length bytes from the specified byte array to this output
      * stream.  Note package private access.
@@ -146,13 +165,6 @@ public final class TelnetOutputStream extends OutputStream {
         output.write(b, 0, b.length);
     }
 
-    // Telnet protocol --------------------------------------------------------
-
-    /**
-     * When true, the last byte the caller passed to write() was a CR.
-     */
-    private boolean writeCR = false;
-
     /**
      * Writes len bytes from the specified byte array starting at offset off
      * to this output stream.
index 923ba2998a59b6f6f39c1a5972e6a103c2ca5a09..7523192602dce2a3cecb7f117ce45a06f5599cd2 100644 (file)
@@ -39,7 +39,14 @@ import java.net.SocketException;
  */
 public final class TelnetServerSocket extends ServerSocket {
 
-    // ServerSocket interface -------------------------------------------------
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Creates an unbound server socket.
@@ -94,6 +101,10 @@ public final class TelnetServerSocket extends ServerSocket {
         super(port, backlog, bindAddr);
     }
 
+    // ------------------------------------------------------------------------
+    // ServerSocket -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Listens for a connection to be made to this socket and accepts it. The
      * method blocks until a connection is made.
index 74ad72a3799e5dad860f94d15a29d0d5cee73b16..31663884c1f48fceb907f22438959cd2f7a23c1f 100644 (file)
@@ -40,15 +40,9 @@ import java.net.Socket;
  */
 public final class TelnetSocket extends Socket {
 
-    /**
-     * The telnet-aware socket InputStream.
-     */
-    private TelnetInputStream input;
-
-    /**
-     * The telnet-aware socket OutputStream.
-     */
-    private TelnetOutputStream output;
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     // Telnet protocol special characters.  Note package private access.
     static final int TELNET_SE         = 240;
@@ -71,6 +65,21 @@ public final class TelnetSocket extends Socket {
     static final int C_LF              = 0x0A;
     static final int C_CR              = 0x0D;
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The telnet-aware socket InputStream.
+     */
+    private TelnetInputStream input;
+
+    /**
+     * The telnet-aware socket OutputStream.
+     */
+    private TelnetOutputStream output;
+
+
     /**
      * If true, this is a server socket (i.e. created by accept()).
      */
@@ -126,14 +135,9 @@ public final class TelnetSocket extends Socket {
      */
     String terminalSpeed = "";
 
-    /**
-     * See if telnet server/client is in ASCII mode.
-     *
-     * @return if true, this connection is in ASCII mode
-     */
-    public boolean isAscii() {
-        return (!binaryMode);
-    }
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Creates a Socket that knows the telnet protocol.  Note package private
@@ -145,7 +149,9 @@ public final class TelnetSocket extends Socket {
         super();
     }
 
-    // Socket interface -------------------------------------------------------
+    // ------------------------------------------------------------------------
+    // Socket -----------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Returns an input stream for this socket.
@@ -181,4 +187,17 @@ public final class TelnetSocket extends Socket {
         return output;
     }
 
+    // ------------------------------------------------------------------------
+    // TelnetSocket -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * See if telnet server/client is in ASCII mode.
+     *
+     * @return if true, this connection is in ASCII mode
+     */
+    public boolean isAscii() {
+        return (!binaryMode);
+    }
+
 }
index 4cc8757544cadf639c23210a1c016b4666a7464f..5110db0edbdb925b3fc93368d919dac401702f68 100644 (file)
@@ -34,11 +34,9 @@ package jexer.tterminal;
  */
 public final class DECCharacterSets {
 
-    /**
-     * Private constructor prevents accidental creation of this class.
-     */
-    private DECCharacterSets() {
-    }
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * US - Normal "international" (ASCII).
@@ -370,4 +368,14 @@ public final class DECCharacterSets {
         0x2084, 0x2085, 0x2086, 0x2087, 0x2088, 0x2089, 0x00B6, 0x0020
     };
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Private constructor prevents accidental creation of this class.
+     */
+    private DECCharacterSets() {
+    }
+
 }
index 15739486de2b6bf386d4641638d69a3e1c94c744..8eff2de1b641fbb61ee4aea910e52459ae8b29d9 100644 (file)
@@ -35,16 +35,68 @@ import jexer.bits.CellAttributes;
  * This represents a single line of the display buffer.
  */
 public final class DisplayLine {
+
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Maximum line length.
      */
     private static final int MAX_LINE_LENGTH = 256;
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The characters/attributes of the line.
      */
     private Cell [] chars;
 
+    /**
+     * Double-width line flag.
+     */
+    private boolean doubleWidth = false;
+
+    /**
+     * Double height line flag.  Valid values are:
+     *
+     * <p><pre>
+     *   0 = single height
+     *   1 = top half double height
+     *   2 = bottom half double height
+     * </pre>
+     */
+    private int doubleHeight = 0;
+
+    /**
+     * DECSCNM - reverse video.  We copy the flag to the line so that
+     * reverse-mode scrollback lines still show inverted colors correctly.
+     */
+    private boolean reverseColor = false;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor sets everything to drawing attributes.
+     *
+     * @param attr current drawing attributes
+     */
+    public DisplayLine(final CellAttributes attr) {
+        chars = new Cell[MAX_LINE_LENGTH];
+        for (int i = 0; i < chars.length; i++) {
+            chars[i] = new Cell();
+            chars[i].setTo(attr);
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // DisplayLine ------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Get the Cell at a specific column.
      *
@@ -64,11 +116,6 @@ public final class DisplayLine {
         return chars.length;
     }
 
-    /**
-     * Double-width line flag.
-     */
-    private boolean doubleWidth = false;
-
     /**
      * Get double width flag.
      *
@@ -87,17 +134,6 @@ public final class DisplayLine {
         this.doubleWidth = doubleWidth;
     }
 
-    /**
-     * Double height line flag.  Valid values are:
-     *
-     * <p><pre>
-     *   0 = single height
-     *   1 = top half double height
-     *   2 = bottom half double height
-     * </pre>
-     */
-    private int doubleHeight = 0;
-
     /**
      * Get double height flag.
      *
@@ -116,12 +152,6 @@ public final class DisplayLine {
         this.doubleHeight = doubleHeight;
     }
 
-    /**
-     * DECSCNM - reverse video.  We copy the flag to the line so that
-     * reverse-mode scrollback lines still show inverted colors correctly.
-     */
-    private boolean reverseColor = false;
-
     /**
      * Get reverse video flag.
      *
@@ -140,19 +170,6 @@ public final class DisplayLine {
         this.reverseColor = reverseColor;
     }
 
-    /**
-     * Public constructor sets everything to drawing attributes.
-     *
-     * @param attr current drawing attributes
-     */
-    public DisplayLine(final CellAttributes attr) {
-        chars = new Cell[MAX_LINE_LENGTH];
-        for (int i = 0; i < chars.length; i++) {
-            chars[i] = new Cell();
-            chars[i].setTo(attr);
-        }
-    }
-
     /**
      * Insert a character at the specified position.
      *
index caa295ad47776ce242557359407ab18b2d7dacf5..7a37a95fdd765cae7e7eb389cc5446fd603ff5d1 100644 (file)
@@ -90,6 +90,10 @@ import static jexer.TKeypress.*;
  */
 public class ECMA48 implements Runnable {
 
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The emulator can emulate several kinds of terminals.
      */
@@ -116,194 +120,115 @@ public class ECMA48 implements Runnable {
     }
 
     /**
-     * Return the proper primary Device Attributes string.
-     *
-     * @return string to send to remote side that is appropriate for the
-     * this.type
+     * Parser character scan states.
      */
-    private String deviceTypeResponse() {
-        switch (type) {
-        case VT100:
-            // "I am a VT100 with advanced video option" (often VT102)
-            return "\033[?1;2c";
-
-        case VT102:
-            // "I am a VT102"
-            return "\033[?6c";
-
-        case VT220:
-        case XTERM:
-            // "I am a VT220" - 7 bit version
-            if (!s8c1t) {
-                return "\033[?62;1;6c";
-            }
-            // "I am a VT220" - 8 bit version
-            return "\u009b?62;1;6c";
-        default:
-            throw new IllegalArgumentException("Invalid device type: " + type);
-        }
+    private enum ScanState {
+        GROUND,
+        ESCAPE,
+        ESCAPE_INTERMEDIATE,
+        CSI_ENTRY,
+        CSI_PARAM,
+        CSI_INTERMEDIATE,
+        CSI_IGNORE,
+        DCS_ENTRY,
+        DCS_INTERMEDIATE,
+        DCS_PARAM,
+        DCS_PASSTHROUGH,
+        DCS_IGNORE,
+        SOSPMAPC_STRING,
+        OSC_STRING,
+        VT52_DIRECT_CURSOR_ADDRESS
     }
 
     /**
-     * Return the proper TERM environment variable for this device type.
-     *
-     * @param deviceType DeviceType.VT100, DeviceType, XTERM, etc.
-     * @return "vt100", "xterm", etc.
+     * The selected number pad mode (DECKPAM, DECKPNM).  We record this, but
+     * can't really use it in keypress() because we do not see number pad
+     * events from TKeypress.
      */
-    public static String deviceTypeTerm(final DeviceType deviceType) {
-        switch (deviceType) {
-        case VT100:
-            return "vt100";
-
-        case VT102:
-            return "vt102";
-
-        case VT220:
-            return "vt220";
-
-        case XTERM:
-            return "xterm";
-
-        default:
-            throw new IllegalArgumentException("Invalid device type: "
-                + deviceType);
-        }
+    private enum KeypadMode {
+        Application,
+        Numeric
     }
 
     /**
-     * Return the proper LANG for this device type.  Only XTERM devices know
-     * about UTF-8, the others are defined by their standard to be either
-     * 7-bit or 8-bit characters only.
-     *
-     * @param deviceType DeviceType.VT100, DeviceType, XTERM, etc.
-     * @param baseLang a base language without UTF-8 flag such as "C" or
-     * "en_US"
-     * @return "en_US", "en_US.UTF-8", etc.
+     * Arrow keys can emit three different sequences (DECCKM or VT52
+     * submode).
      */
-    public static String deviceTypeLang(final DeviceType deviceType,
-        final String baseLang) {
-
-        switch (deviceType) {
-
-        case VT100:
-        case VT102:
-        case VT220:
-            return baseLang;
-
-        case XTERM:
-            return baseLang + ".UTF-8";
-
-        default:
-            throw new IllegalArgumentException("Invalid device type: "
-                + deviceType);
-        }
+    private enum ArrowKeyMode {
+        VT52,
+        ANSI,
+        VT100
     }
 
     /**
-     * Write a string directly to the remote side.
-     *
-     * @param str string to send
+     * Available character sets for GL, GR, G0, G1, G2, G3.
      */
-    public void writeRemote(final String str) {
-        if (stopReaderThread) {
-            // Reader hit EOF, bail out now.
-            close();
-            return;
-        }
-
-        // System.err.printf("writeRemote() '%s'\n", str);
+    private enum CharacterSet {
+        US,
+        UK,
+        DRAWING,
+        ROM,
+        ROM_SPECIAL,
+        VT52_GRAPHICS,
+        DEC_SUPPLEMENTAL,
+        NRC_DUTCH,
+        NRC_FINNISH,
+        NRC_FRENCH,
+        NRC_FRENCH_CA,
+        NRC_GERMAN,
+        NRC_ITALIAN,
+        NRC_NORWEGIAN,
+        NRC_SPANISH,
+        NRC_SWEDISH,
+        NRC_SWISS
+    }
 
-        switch (type) {
-        case VT100:
-        case VT102:
-        case VT220:
-            if (outputStream == null) {
-                return;
-            }
-            try {
-                outputStream.flush();
-                for (int i = 0; i < str.length(); i++) {
-                    outputStream.write(str.charAt(i));
-                }
-                outputStream.flush();
-            } catch (IOException e) {
-                // Assume EOF
-                close();
-            }
-            break;
-        case XTERM:
-            if (output == null) {
-                return;
-            }
-            try {
-                output.flush();
-                output.write(str);
-                output.flush();
-            } catch (IOException e) {
-                // Assume EOF
-                close();
-            }
-            break;
-        default:
-            throw new IllegalArgumentException("Invalid device type: " + type);
-        }
+    /**
+     * Single-shift states used by the C1 control characters SS2 (0x8E) and
+     * SS3 (0x8F).
+     */
+    private enum Singleshift {
+        NONE,
+        SS2,
+        SS3
     }
 
     /**
-     * Close the input and output streams and stop the reader thread.  Note
-     * that it is safe to call this multiple times.
+     * VT220+ lockshift states.
      */
-    public final void close() {
+    private enum LockshiftMode {
+        NONE,
+        G1_GR,
+        G2_GR,
+        G2_GL,
+        G3_GR,
+        G3_GL
+    }
 
-        // Tell the reader thread to stop looking at input.  It will close
-        // the input streams.
-        if (stopReaderThread == false) {
-            stopReaderThread = true;
-            try {
-                readerThread.join(1000);
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
-        }
+    /**
+     * XTERM mouse reporting protocols.
+     */
+    private enum MouseProtocol {
+        OFF,
+        X10,
+        NORMAL,
+        BUTTONEVENT,
+        ANYEVENT
+    }
 
-        // Now close the output stream.
-        switch (type) {
-        case VT100:
-        case VT102:
-        case VT220:
-            if (outputStream != null) {
-                try {
-                    outputStream.close();
-                } catch (IOException e) {
-                    // SQUASH
-                }
-                outputStream = null;
-            }
-            break;
-        case XTERM:
-            if (outputStream != null) {
-                try {
-                    outputStream.close();
-                } catch (IOException e) {
-                    // SQUASH
-                }
-                outputStream = null;
-            }
-            if (output != null) {
-                try {
-                    output.close();
-                } catch (IOException e) {
-                    // SQUASH
-                }
-                output = null;
-            }
-            break;
-        default:
-            throw new IllegalArgumentException("Invalid device type: " +
-                type);
-        }
+    /**
+     * XTERM mouse reporting encodings.
+     */
+    private enum MouseEncoding {
+        X10,
+        UTF8,
+        SGR
     }
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * The enclosing listening object.
      */
@@ -320,58 +245,20 @@ public class ECMA48 implements Runnable {
     private Thread readerThread = null;
 
     /**
-     * See if the reader thread is still running.
-     *
-     * @return if true, we are still connected to / reading from the remote
-     * side
-     */
-    public final boolean isReading() {
-        return (!stopReaderThread);
-    }
-
-    /**
-     * The type of emulator to be.
+     * The type of emulator to be.
      */
     private DeviceType type = DeviceType.VT102;
 
-    /**
-     * Obtain a new blank display line for an external user
-     * (e.g. TTerminalWindow).
-     *
-     * @return new blank line
-     */
-    public final DisplayLine getBlankDisplayLine() {
-        return new DisplayLine(currentState.attr);
-    }
-
     /**
      * The scrollback buffer characters + attributes.
      */
     private volatile List<DisplayLine> scrollback;
 
-    /**
-     * Get the scrollback buffer.
-     *
-     * @return the scrollback buffer
-     */
-    public final List<DisplayLine> getScrollbackBuffer() {
-        return scrollback;
-    }
-
     /**
      * The raw display buffer characters + attributes.
      */
     private volatile List<DisplayLine> display;
 
-    /**
-     * Get the display buffer.
-     *
-     * @return the display buffer
-     */
-    public final List<DisplayLine> getDisplayBuffer() {
-        return display;
-    }
-
     /**
      * The terminal's input.  For type == XTERM, this is an InputStreamReader
      * with UTF-8 encoding.
@@ -394,122 +281,16 @@ public class ECMA48 implements Runnable {
      */
     private OutputStream outputStream;
 
-    /**
-     * Parser character scan states.
-     */
-    enum ScanState {
-        GROUND,
-        ESCAPE,
-        ESCAPE_INTERMEDIATE,
-        CSI_ENTRY,
-        CSI_PARAM,
-        CSI_INTERMEDIATE,
-        CSI_IGNORE,
-        DCS_ENTRY,
-        DCS_INTERMEDIATE,
-        DCS_PARAM,
-        DCS_PASSTHROUGH,
-        DCS_IGNORE,
-        SOSPMAPC_STRING,
-        OSC_STRING,
-        VT52_DIRECT_CURSOR_ADDRESS
-    }
-
     /**
      * Current scanning state.
      */
     private ScanState scanState;
 
-    /**
-     * The selected number pad mode (DECKPAM, DECKPNM).  We record this, but
-     * can't really use it in keypress() because we do not see number pad
-     * events from TKeypress.
-     */
-    private enum KeypadMode {
-        Application,
-        Numeric
-    }
-
-    /**
-     * Arrow keys can emit three different sequences (DECCKM or VT52
-     * submode).
-     */
-    private enum ArrowKeyMode {
-        VT52,
-        ANSI,
-        VT100
-    }
-
-    /**
-     * Available character sets for GL, GR, G0, G1, G2, G3.
-     */
-    private enum CharacterSet {
-        US,
-        UK,
-        DRAWING,
-        ROM,
-        ROM_SPECIAL,
-        VT52_GRAPHICS,
-        DEC_SUPPLEMENTAL,
-        NRC_DUTCH,
-        NRC_FINNISH,
-        NRC_FRENCH,
-        NRC_FRENCH_CA,
-        NRC_GERMAN,
-        NRC_ITALIAN,
-        NRC_NORWEGIAN,
-        NRC_SPANISH,
-        NRC_SWEDISH,
-        NRC_SWISS
-    }
-
-    /**
-     * Single-shift states used by the C1 control characters SS2 (0x8E) and
-     * SS3 (0x8F).
-     */
-    private enum Singleshift {
-        NONE,
-        SS2,
-        SS3
-    }
-
-    /**
-     * VT220+ lockshift states.
-     */
-    private enum LockshiftMode {
-        NONE,
-        G1_GR,
-        G2_GR,
-        G2_GL,
-        G3_GR,
-        G3_GL
-    }
-
-    /**
-     * XTERM mouse reporting protocols.
-     */
-    private enum MouseProtocol {
-        OFF,
-        X10,
-        NORMAL,
-        BUTTONEVENT,
-        ANYEVENT
-    }
-
     /**
      * Which mouse protocol is active.
      */
     private MouseProtocol mouseProtocol = MouseProtocol.OFF;
 
-    /**
-     * XTERM mouse reporting encodings.
-     */
-    private enum MouseEncoding {
-        X10,
-        UTF8,
-        SGR
-    }
-
     /**
      * Which mouse encoding is active.
      */
@@ -521,77 +302,12 @@ public class ECMA48 implements Runnable {
      */
     private int width;
 
-    /**
-     * Get the display width.
-     *
-     * @return the width (usually 80 or 132)
-     */
-    public final int getWidth() {
-        return width;
-    }
-
-    /**
-     * Set the display width.
-     *
-     * @param width the new width
-     */
-    public final void setWidth(final int width) {
-        this.width = width;
-        rightMargin = width - 1;
-        if (currentState.cursorX >= width) {
-            currentState.cursorX = width - 1;
-        }
-        if (savedState.cursorX >= width) {
-            savedState.cursorX = width - 1;
-        }
-    }
-
     /**
      * Physical display height.  We start at 80x24, but the user can resize
      * us bigger/smaller.
      */
     private int height;
 
-    /**
-     * Get the display height.
-     *
-     * @return the height (usually 24)
-     */
-    public final int getHeight() {
-        return height;
-    }
-
-    /**
-     * Set the display height.
-     *
-     * @param height the new height
-     */
-    public final void setHeight(final int height) {
-        int delta = height - this.height;
-        this.height = height;
-        scrollRegionBottom += delta;
-        if (scrollRegionBottom < 0) {
-            scrollRegionBottom = height;
-        }
-        if (scrollRegionTop >= scrollRegionBottom) {
-            scrollRegionTop = 0;
-        }
-        if (currentState.cursorY >= height) {
-            currentState.cursorY = height - 1;
-        }
-        if (savedState.cursorY >= height) {
-            savedState.cursorY = height - 1;
-        }
-        while (display.size() < height) {
-            DisplayLine line = new DisplayLine(currentState.attr);
-            line.setReverseColor(reverseVideo);
-            display.add(line);
-        }
-        while (display.size() > height) {
-            scrollback.add(display.remove(0));
-        }
-    }
-
     /**
      * Top margin of the scrolling region.
      */
@@ -641,32 +357,12 @@ public class ECMA48 implements Runnable {
      */
     private boolean cursorVisible = true;
 
-    /**
-     * Get visible cursor flag.
-     *
-     * @return if true, the cursor is visible
-     */
-    public final boolean isCursorVisible() {
-        return cursorVisible;
-    }
-
     /**
      * Screen title as set by the xterm OSC sequence.  Lots of applications
      * send a screenTitle regardless of whether it is an xterm client or not.
      */
     private String screenTitle = "";
 
-    /**
-     * Get the screen title as set by the xterm OSC sequence.  Lots of
-     * applications send a screenTitle regardless of whether it is an xterm
-     * client or not.
-     *
-     * @return screen title
-     */
-    public final String getScreenTitle() {
-        return screenTitle;
-    }
-
     /**
      * Parameter characters being collected.
      */
@@ -722,15 +418,6 @@ public class ECMA48 implements Runnable {
      */
     private boolean columns132 = false;
 
-    /**
-     * Get 132 columns value.
-     *
-     * @return if true, the terminal is in 132 column mode
-     */
-    public final boolean isColumns132() {
-                return columns132;
-        }
-
     /**
      * true = reverse video.  Set by DECSCNM.
      */
@@ -741,6 +428,16 @@ public class ECMA48 implements Runnable {
      */
     private boolean fullDuplex = true;
 
+    /**
+     * The current terminal state.
+     */
+    private SaveableState currentState;
+
+    /**
+     * The last saved terminal state.
+     */
+    private SaveableState savedState;
+
     /**
      * DECSC/DECRC save/restore a subset of the total state.  This class
      * encapsulates those specific flags/modes.
@@ -846,23 +543,520 @@ public class ECMA48 implements Runnable {
             this.lineWrap       = that.lineWrap;
         }
 
-        /**
-         * Public constructor.
-         */
-        public SaveableState() {
-            reset();
+        /**
+         * Public constructor.
+         */
+        public SaveableState() {
+            reset();
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param type one of the DeviceType constants to select VT100, VT102,
+     * VT220, or XTERM
+     * @param inputStream an InputStream connected to the remote side.  For
+     * type == XTERM, inputStream is converted to a Reader with UTF-8
+     * encoding.
+     * @param outputStream an OutputStream connected to the remote user.  For
+     * type == XTERM, outputStream is converted to a Writer with UTF-8
+     * encoding.
+     * @param displayListener a callback to the outer display, or null for
+     * default VT100 behavior
+     * @throws UnsupportedEncodingException if an exception is thrown when
+     * creating the InputStreamReader
+     */
+    public ECMA48(final DeviceType type, final InputStream inputStream,
+        final OutputStream outputStream, final DisplayListener displayListener)
+        throws UnsupportedEncodingException {
+
+        assert (inputStream != null);
+        assert (outputStream != null);
+
+        csiParams         = new ArrayList<Integer>();
+        tabStops          = new ArrayList<Integer>();
+        scrollback        = new LinkedList<DisplayLine>();
+        display           = new LinkedList<DisplayLine>();
+
+        this.type         = type;
+        if (inputStream instanceof TimeoutInputStream) {
+            this.inputStream  = (TimeoutInputStream)inputStream;
+        } else {
+            this.inputStream  = new TimeoutInputStream(inputStream, 2000);
+        }
+        if (type == DeviceType.XTERM) {
+            this.input    = new InputStreamReader(this.inputStream, "UTF-8");
+            this.output   = new OutputStreamWriter(new
+                BufferedOutputStream(outputStream), "UTF-8");
+            this.outputStream = null;
+        } else {
+            this.output       = null;
+            this.outputStream = new BufferedOutputStream(outputStream);
+        }
+        this.displayListener  = displayListener;
+
+        reset();
+        for (int i = 0; i < height; i++) {
+            display.add(new DisplayLine(currentState.attr));
+        }
+
+        // Spin up the input reader
+        readerThread = new Thread(this);
+        readerThread.start();
+    }
+
+    // ------------------------------------------------------------------------
+    // Runnable ---------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Read function runs on a separate thread.
+     */
+    public final void run() {
+        boolean utf8 = false;
+        boolean done = false;
+
+        if (type == DeviceType.XTERM) {
+            utf8 = true;
+        }
+
+        // available() will often return > 1, so we need to read in chunks to
+        // stay caught up.
+        char [] readBufferUTF8 = null;
+        byte [] readBuffer = null;
+        if (utf8) {
+            readBufferUTF8 = new char[128];
+        } else {
+            readBuffer = new byte[128];
+        }
+
+        while (!done && !stopReaderThread) {
+            try {
+                int n = inputStream.available();
+
+                // System.err.printf("available() %d\n", n); System.err.flush();
+                if (utf8) {
+                    if (readBufferUTF8.length < n) {
+                        // The buffer wasn't big enough, make it huger
+                        int newSizeHalf = Math.max(readBufferUTF8.length,
+                            n);
+
+                        readBufferUTF8 = new char[newSizeHalf * 2];
+                    }
+                } else {
+                    if (readBuffer.length < n) {
+                        // The buffer wasn't big enough, make it huger
+                        int newSizeHalf = Math.max(readBuffer.length, n);
+                        readBuffer = new byte[newSizeHalf * 2];
+                    }
+                }
+                if (n == 0) {
+                    try {
+                        Thread.sleep(2);
+                    } catch (InterruptedException e) {
+                        // SQUASH
+                    }
+                    continue;
+                }
+
+                int rc = -1;
+                try {
+                    if (utf8) {
+                        rc = input.read(readBufferUTF8, 0,
+                            readBufferUTF8.length);
+                    } else {
+                        rc = inputStream.read(readBuffer, 0,
+                            readBuffer.length);
+                    }
+                } catch (ReadTimeoutException e) {
+                    rc = 0;
+                }
+
+                // System.err.printf("read() %d\n", rc); System.err.flush();
+                if (rc == -1) {
+                    // This is EOF
+                    done = true;
+                } else {
+                    // Don't step on UI events
+                    synchronized (this) {
+                        for (int i = 0; i < rc; i++) {
+                            int ch = 0;
+                            if (utf8) {
+                                ch = readBufferUTF8[i];
+                            } else {
+                                ch = readBuffer[i];
+                            }
+
+                            consume((char)ch);
+                        }
+                    }
+                    // Permit my enclosing UI to know that I updated.
+                    if (displayListener != null) {
+                        displayListener.displayChanged();
+                    }
+                }
+                // System.err.println("end while loop"); System.err.flush();
+            } catch (IOException e) {
+                e.printStackTrace();
+                done = true;
+            }
+
+        } // while ((done == false) && (stopReaderThread == false))
+
+        // Let the rest of the world know that I am done.
+        stopReaderThread = true;
+
+        try {
+            inputStream.cancelRead();
+            inputStream.close();
+            inputStream = null;
+        } catch (IOException e) {
+            // SQUASH
+        }
+        try {
+            input.close();
+            input = null;
+        } catch (IOException e) {
+            // SQUASH
+        }
+
+        // Permit my enclosing UI to know that I updated.
+        if (displayListener != null) {
+            displayListener.displayChanged();
+        }
+
+        // System.err.println("*** run() exiting..."); System.err.flush();
+    }
+
+    // ------------------------------------------------------------------------
+    // ECMA48 -----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Return the proper primary Device Attributes string.
+     *
+     * @return string to send to remote side that is appropriate for the
+     * this.type
+     */
+    private String deviceTypeResponse() {
+        switch (type) {
+        case VT100:
+            // "I am a VT100 with advanced video option" (often VT102)
+            return "\033[?1;2c";
+
+        case VT102:
+            // "I am a VT102"
+            return "\033[?6c";
+
+        case VT220:
+        case XTERM:
+            // "I am a VT220" - 7 bit version
+            if (!s8c1t) {
+                return "\033[?62;1;6c";
+            }
+            // "I am a VT220" - 8 bit version
+            return "\u009b?62;1;6c";
+        default:
+            throw new IllegalArgumentException("Invalid device type: " + type);
+        }
+    }
+
+    /**
+     * Return the proper TERM environment variable for this device type.
+     *
+     * @param deviceType DeviceType.VT100, DeviceType, XTERM, etc.
+     * @return "vt100", "xterm", etc.
+     */
+    public static String deviceTypeTerm(final DeviceType deviceType) {
+        switch (deviceType) {
+        case VT100:
+            return "vt100";
+
+        case VT102:
+            return "vt102";
+
+        case VT220:
+            return "vt220";
+
+        case XTERM:
+            return "xterm";
+
+        default:
+            throw new IllegalArgumentException("Invalid device type: "
+                + deviceType);
+        }
+    }
+
+    /**
+     * Return the proper LANG for this device type.  Only XTERM devices know
+     * about UTF-8, the others are defined by their standard to be either
+     * 7-bit or 8-bit characters only.
+     *
+     * @param deviceType DeviceType.VT100, DeviceType, XTERM, etc.
+     * @param baseLang a base language without UTF-8 flag such as "C" or
+     * "en_US"
+     * @return "en_US", "en_US.UTF-8", etc.
+     */
+    public static String deviceTypeLang(final DeviceType deviceType,
+        final String baseLang) {
+
+        switch (deviceType) {
+
+        case VT100:
+        case VT102:
+        case VT220:
+            return baseLang;
+
+        case XTERM:
+            return baseLang + ".UTF-8";
+
+        default:
+            throw new IllegalArgumentException("Invalid device type: "
+                + deviceType);
+        }
+    }
+
+    /**
+     * Write a string directly to the remote side.
+     *
+     * @param str string to send
+     */
+    public void writeRemote(final String str) {
+        if (stopReaderThread) {
+            // Reader hit EOF, bail out now.
+            close();
+            return;
+        }
+
+        // System.err.printf("writeRemote() '%s'\n", str);
+
+        switch (type) {
+        case VT100:
+        case VT102:
+        case VT220:
+            if (outputStream == null) {
+                return;
+            }
+            try {
+                outputStream.flush();
+                for (int i = 0; i < str.length(); i++) {
+                    outputStream.write(str.charAt(i));
+                }
+                outputStream.flush();
+            } catch (IOException e) {
+                // Assume EOF
+                close();
+            }
+            break;
+        case XTERM:
+            if (output == null) {
+                return;
+            }
+            try {
+                output.flush();
+                output.write(str);
+                output.flush();
+            } catch (IOException e) {
+                // Assume EOF
+                close();
+            }
+            break;
+        default:
+            throw new IllegalArgumentException("Invalid device type: " + type);
+        }
+    }
+
+    /**
+     * Close the input and output streams and stop the reader thread.  Note
+     * that it is safe to call this multiple times.
+     */
+    public final void close() {
+
+        // Tell the reader thread to stop looking at input.  It will close
+        // the input streams.
+        if (stopReaderThread == false) {
+            stopReaderThread = true;
+            try {
+                readerThread.join(1000);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+
+        // Now close the output stream.
+        switch (type) {
+        case VT100:
+        case VT102:
+        case VT220:
+            if (outputStream != null) {
+                try {
+                    outputStream.close();
+                } catch (IOException e) {
+                    // SQUASH
+                }
+                outputStream = null;
+            }
+            break;
+        case XTERM:
+            if (outputStream != null) {
+                try {
+                    outputStream.close();
+                } catch (IOException e) {
+                    // SQUASH
+                }
+                outputStream = null;
+            }
+            if (output != null) {
+                try {
+                    output.close();
+                } catch (IOException e) {
+                    // SQUASH
+                }
+                output = null;
+            }
+            break;
+        default:
+            throw new IllegalArgumentException("Invalid device type: " +
+                type);
+        }
+    }
+
+    /**
+     * See if the reader thread is still running.
+     *
+     * @return if true, we are still connected to / reading from the remote
+     * side
+     */
+    public final boolean isReading() {
+        return (!stopReaderThread);
+    }
+
+    /**
+     * Obtain a new blank display line for an external user
+     * (e.g. TTerminalWindow).
+     *
+     * @return new blank line
+     */
+    public final DisplayLine getBlankDisplayLine() {
+        return new DisplayLine(currentState.attr);
+    }
+
+    /**
+     * Get the scrollback buffer.
+     *
+     * @return the scrollback buffer
+     */
+    public final List<DisplayLine> getScrollbackBuffer() {
+        return scrollback;
+    }
+
+    /**
+     * Get the display buffer.
+     *
+     * @return the display buffer
+     */
+    public final List<DisplayLine> getDisplayBuffer() {
+        return display;
+    }
+
+    /**
+     * Get the display width.
+     *
+     * @return the width (usually 80 or 132)
+     */
+    public final int getWidth() {
+        return width;
+    }
+
+    /**
+     * Set the display width.
+     *
+     * @param width the new width
+     */
+    public final void setWidth(final int width) {
+        this.width = width;
+        rightMargin = width - 1;
+        if (currentState.cursorX >= width) {
+            currentState.cursorX = width - 1;
+        }
+        if (savedState.cursorX >= width) {
+            savedState.cursorX = width - 1;
+        }
+    }
+
+    /**
+     * Get the display height.
+     *
+     * @return the height (usually 24)
+     */
+    public final int getHeight() {
+        return height;
+    }
+
+    /**
+     * Set the display height.
+     *
+     * @param height the new height
+     */
+    public final void setHeight(final int height) {
+        int delta = height - this.height;
+        this.height = height;
+        scrollRegionBottom += delta;
+        if (scrollRegionBottom < 0) {
+            scrollRegionBottom = height;
+        }
+        if (scrollRegionTop >= scrollRegionBottom) {
+            scrollRegionTop = 0;
+        }
+        if (currentState.cursorY >= height) {
+            currentState.cursorY = height - 1;
+        }
+        if (savedState.cursorY >= height) {
+            savedState.cursorY = height - 1;
+        }
+        while (display.size() < height) {
+            DisplayLine line = new DisplayLine(currentState.attr);
+            line.setReverseColor(reverseVideo);
+            display.add(line);
+        }
+        while (display.size() > height) {
+            scrollback.add(display.remove(0));
         }
     }
 
     /**
-     * The current terminal state.
+     * Get visible cursor flag.
+     *
+     * @return if true, the cursor is visible
      */
-    private SaveableState currentState;
+    public final boolean isCursorVisible() {
+        return cursorVisible;
+    }
 
     /**
-     * The last saved terminal state.
+     * Get the screen title as set by the xterm OSC sequence.  Lots of
+     * applications send a screenTitle regardless of whether it is an xterm
+     * client or not.
+     *
+     * @return screen title
      */
-    private SaveableState savedState;
+    public final String getScreenTitle() {
+        return screenTitle;
+    }
+
+    /**
+     * Get 132 columns value.
+     *
+     * @return if true, the terminal is in 132 column mode
+     */
+    public final boolean isColumns132() {
+        return columns132;
+    }
 
     /**
      * Clear the CSI parameters and flags.
@@ -932,61 +1126,6 @@ public class ECMA48 implements Runnable {
         toGround();
     }
 
-    /**
-     * Public constructor.
-     *
-     * @param type one of the DeviceType constants to select VT100, VT102,
-     * VT220, or XTERM
-     * @param inputStream an InputStream connected to the remote side.  For
-     * type == XTERM, inputStream is converted to a Reader with UTF-8
-     * encoding.
-     * @param outputStream an OutputStream connected to the remote user.  For
-     * type == XTERM, outputStream is converted to a Writer with UTF-8
-     * encoding.
-     * @param displayListener a callback to the outer display, or null for
-     * default VT100 behavior
-     * @throws UnsupportedEncodingException if an exception is thrown when
-     * creating the InputStreamReader
-     */
-    public ECMA48(final DeviceType type, final InputStream inputStream,
-        final OutputStream outputStream, final DisplayListener displayListener)
-        throws UnsupportedEncodingException {
-
-        assert (inputStream != null);
-        assert (outputStream != null);
-
-        csiParams         = new ArrayList<Integer>();
-        tabStops          = new ArrayList<Integer>();
-        scrollback        = new LinkedList<DisplayLine>();
-        display           = new LinkedList<DisplayLine>();
-
-        this.type         = type;
-        if (inputStream instanceof TimeoutInputStream) {
-            this.inputStream  = (TimeoutInputStream)inputStream;
-        } else {
-            this.inputStream  = new TimeoutInputStream(inputStream, 2000);
-        }
-        if (type == DeviceType.XTERM) {
-            this.input    = new InputStreamReader(this.inputStream, "UTF-8");
-            this.output   = new OutputStreamWriter(new
-                BufferedOutputStream(outputStream), "UTF-8");
-            this.outputStream = null;
-        } else {
-            this.output       = null;
-            this.outputStream = new BufferedOutputStream(outputStream);
-        }
-        this.displayListener  = displayListener;
-
-        reset();
-        for (int i = 0; i < height; i++) {
-            display.add(new DisplayLine(currentState.attr));
-        }
-
-        // Spin up the input reader
-        readerThread = new Thread(this);
-        readerThread.start();
-    }
-
     /**
      * Append a new line to the bottom of the display, adding lines off the
      * top to the scrollback buffer.
@@ -6015,123 +6154,4 @@ public class ECMA48 implements Runnable {
         return currentState.cursorY;
     }
 
-    /**
-     * Read function runs on a separate thread.
-     */
-    public final void run() {
-        boolean utf8 = false;
-        boolean done = false;
-
-        if (type == DeviceType.XTERM) {
-            utf8 = true;
-        }
-
-        // available() will often return > 1, so we need to read in chunks to
-        // stay caught up.
-        char [] readBufferUTF8 = null;
-        byte [] readBuffer = null;
-        if (utf8) {
-            readBufferUTF8 = new char[128];
-        } else {
-            readBuffer = new byte[128];
-        }
-
-        while (!done && !stopReaderThread) {
-            try {
-                int n = inputStream.available();
-
-                // System.err.printf("available() %d\n", n); System.err.flush();
-                if (utf8) {
-                    if (readBufferUTF8.length < n) {
-                        // The buffer wasn't big enough, make it huger
-                        int newSizeHalf = Math.max(readBufferUTF8.length,
-                            n);
-
-                        readBufferUTF8 = new char[newSizeHalf * 2];
-                    }
-                } else {
-                    if (readBuffer.length < n) {
-                        // The buffer wasn't big enough, make it huger
-                        int newSizeHalf = Math.max(readBuffer.length, n);
-                        readBuffer = new byte[newSizeHalf * 2];
-                    }
-                }
-                if (n == 0) {
-                    try {
-                        Thread.sleep(2);
-                    } catch (InterruptedException e) {
-                        // SQUASH
-                    }
-                    continue;
-                }
-
-                int rc = -1;
-                try {
-                    if (utf8) {
-                        rc = input.read(readBufferUTF8, 0,
-                            readBufferUTF8.length);
-                    } else {
-                        rc = inputStream.read(readBuffer, 0,
-                            readBuffer.length);
-                    }
-                } catch (ReadTimeoutException e) {
-                    rc = 0;
-                }
-
-                // System.err.printf("read() %d\n", rc); System.err.flush();
-                if (rc == -1) {
-                    // This is EOF
-                    done = true;
-                } else {
-                    // Don't step on UI events
-                    synchronized (this) {
-                        for (int i = 0; i < rc; i++) {
-                            int ch = 0;
-                            if (utf8) {
-                                ch = readBufferUTF8[i];
-                            } else {
-                                ch = readBuffer[i];
-                            }
-
-                            consume((char)ch);
-                        }
-                    }
-                    // Permit my enclosing UI to know that I updated.
-                    if (displayListener != null) {
-                        displayListener.displayChanged();
-                    }
-                }
-                // System.err.println("end while loop"); System.err.flush();
-            } catch (IOException e) {
-                e.printStackTrace();
-                done = true;
-            }
-
-        } // while ((done == false) && (stopReaderThread == false))
-
-        // Let the rest of the world know that I am done.
-        stopReaderThread = true;
-
-        try {
-            inputStream.cancelRead();
-            inputStream.close();
-            inputStream = null;
-        } catch (IOException e) {
-            // SQUASH
-        }
-        try {
-            input.close();
-            input = null;
-        } catch (IOException e) {
-            // SQUASH
-        }
-
-        // Permit my enclosing UI to know that I updated.
-        if (displayListener != null) {
-            displayListener.displayChanged();
-        }
-
-        // System.err.println("*** run() exiting..."); System.err.flush();
-    }
-
 }
similarity index 80%
rename from src/jexer/TDirectoryTreeItem.java
rename to src/jexer/ttree/TDirectoryTreeItem.java
index 6d5d018004775c97ae1b476018c5c30f398ce996..c260d7fb95212c7920fce537f5deb74066553b9f 100644 (file)
@@ -26,7 +26,7 @@
  * @author Kevin Lamonte [kevin.lamonte@gmail.com]
  * @version 1
  */
-package jexer;
+package jexer.ttree;
 
 import java.io.File;
 import java.io.IOException;
@@ -34,116 +34,41 @@ import java.util.Collections;
 import java.util.List;
 import java.util.LinkedList;
 
+import jexer.TWidget;
+import jexer.ttree.TTreeViewWidget;
+
 /**
  * TDirectoryTreeItem is a single item in a disk directory tree view.
  */
 public class TDirectoryTreeItem extends TTreeItem {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * File corresponding to this list item.
      */
     private File file;
 
     /**
-     * Get the File corresponding to this list item.
-     *
-     * @return the File
+     * The TTreeViewWidget containing this directory tree.
      */
-    public final File getFile() {
-        return file;
-    }
+    private TTreeViewWidget treeViewWidget;
 
-    /**
-     * Called when this item is expanded or collapsed.  this.expanded will be
-     * true if this item was just expanded from a mouse click or keypress.
-     */
-    @Override
-    public final void onExpand() {
-        // System.err.printf("onExpand() %s\n", file);
-
-        if (file == null) {
-            return;
-        }
-        getChildren().clear();
-
-        // Make sure we can read it before trying to.
-        if (file.canRead()) {
-            setSelectable(true);
-        } else {
-            setSelectable(false);
-        }
-        assert (file.isDirectory());
-        setExpandable(true);
-
-        if (!isExpanded() || !isExpandable()) {
-            getTreeView().reflowData();
-            return;
-        }
-
-        for (File f: file.listFiles()) {
-            // System.err.printf("   -> file %s %s\n", file, file.getName());
-
-            if (f.getName().startsWith(".")) {
-                // Hide dot-files
-                continue;
-            }
-            if (!f.isDirectory()) {
-                continue;
-            }
-
-            try {
-                TDirectoryTreeItem item = new TDirectoryTreeItem(getTreeView(),
-                    f.getCanonicalPath(), false, false);
-
-                item.level = this.level + 1;
-                getChildren().add(item);
-            } catch (IOException e) {
-                continue;
-            }
-        }
-        Collections.sort(getChildren());
-
-        getTreeView().reflowData();
-    }
-
-    /**
-     * Add a child item.  This method should never be used, it will throw an
-     * IllegalArgumentException every time.
-     *
-     * @param text text for this item
-     * @param expanded if true, have it expanded immediately
-     * @return the new item
-     * @throws IllegalArgumentException if this function is called
-     */
-    @Override
-    public final TTreeItem addChild(final String text,
-        final boolean expanded) throws IllegalArgumentException {
-
-        throw new IllegalArgumentException("Do not call addChild(), use onExpand() instead");
-    }
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Public constructor.
      *
-     * @param view root TTreeView
-     * @param text text for this item
-     * @throws IOException if a java.io operation throws
-     */
-    public TDirectoryTreeItem(final TTreeView view,
-        final String text) throws IOException {
-
-        this(view, text, false, true);
-    }
-
-    /**
-     * Public constructor.
-     *
-     * @param view root TTreeView
+     * @param view root TTreeViewWidget
      * @param text text for this item
      * @param expanded if true, have it expanded immediately
      * @throws IOException if a java.io operation throws
      */
-    public TDirectoryTreeItem(final TTreeView view, final String text,
+    public TDirectoryTreeItem(final TTreeViewWidget view, final String text,
         final boolean expanded) throws IOException {
 
         this(view, text, expanded, true);
@@ -152,17 +77,19 @@ public class TDirectoryTreeItem extends TTreeItem {
     /**
      * Public constructor.
      *
-     * @param view root TTreeView
+     * @param view root TTreeViewWidget
      * @param text text for this item
      * @param expanded if true, have it expanded immediately
      * @param openParents if true, expand all paths up the root path and
      * return the root path entry
      * @throws IOException if a java.io operation throws
      */
-    public TDirectoryTreeItem(final TTreeView view, final String text,
+    public TDirectoryTreeItem(final TTreeViewWidget view, final String text,
         final boolean expanded, final boolean openParents) throws IOException {
 
-        super(view, text, false);
+        super(view.getTreeView(), text, false);
+
+        this.treeViewWidget = view;
 
         List<String> parentFiles = new LinkedList<String>();
         boolean oldExpanded = expanded;
@@ -209,9 +136,74 @@ public class TDirectoryTreeItem extends TTreeItem {
                 }
             }
             unselect();
-            getTreeView().setSelected(childFile);
+            getTreeView().setSelected(childFile, true);
             setExpanded(oldExpanded);
         }
-        getTreeView().reflowData();
+
+        view.reflowData();
+    }
+
+    // ------------------------------------------------------------------------
+    // TTreeItem --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the File corresponding to this list item.
+     *
+     * @return the File
+     */
+    public final File getFile() {
+        return file;
+    }
+
+    /**
+     * Called when this item is expanded or collapsed.  this.expanded will be
+     * true if this item was just expanded from a mouse click or keypress.
+     */
+    @Override
+    public final void onExpand() {
+        // System.err.printf("onExpand() %s\n", file);
+
+        if (file == null) {
+            return;
+        }
+        getChildren().clear();
+
+        // Make sure we can read it before trying to.
+        if (file.canRead()) {
+            setSelectable(true);
+        } else {
+            setSelectable(false);
+        }
+        assert (file.isDirectory());
+        setExpandable(true);
+
+        if (!isExpanded() || !isExpandable()) {
+            return;
+        }
+
+        for (File f: file.listFiles()) {
+            // System.err.printf("   -> file %s %s\n", file, file.getName());
+
+            if (f.getName().startsWith(".")) {
+                // Hide dot-files
+                continue;
+            }
+            if (!f.isDirectory()) {
+                continue;
+            }
+
+            try {
+                TDirectoryTreeItem item = new TDirectoryTreeItem(treeViewWidget,
+                    f.getCanonicalPath(), false, false);
+
+                item.level = this.level + 1;
+                getChildren().add(item);
+            } catch (IOException e) {
+                continue;
+            }
+        }
+        Collections.sort(getChildren());
     }
+
 }
similarity index 76%
rename from src/jexer/TTreeItem.java
rename to src/jexer/ttree/TTreeItem.java
index 7de5e123587fe16aa06a62ab5737f27afa5d41ff..901ce8590a78095bdf00a5831f86331b5b84d9e9 100644 (file)
  * @author Kevin Lamonte [kevin.lamonte@gmail.com]
  * @version 1
  */
-package jexer;
+package jexer.ttree;
 
 import java.util.ArrayList;
 import java.util.List;
 
+import jexer.TWidget;
 import jexer.bits.CellAttributes;
 import jexer.bits.GraphicsChars;
 import jexer.event.TKeypressEvent;
@@ -42,164 +43,55 @@ import static jexer.TKeypress.*;
  */
 public class TTreeItem extends TWidget {
 
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Hang onto reference to my parent TTreeView so I can call its reflow()
      * when I add a child node.
      */
     private TTreeView view;
 
-    /**
-     * Get the parent TTreeView.
-     *
-     * @return the parent TTreeView
-     */
-    public final TTreeView getTreeView() {
-        return view;
-    }
-
     /**
      * Displayable text for this item.
      */
     private String text;
 
-    /**
-     * Get the displayable text for this item.
-     *
-     * @return the displayable text for this item
-     */
-    public final String getText() {
-        return text;
-    }
-
-    /**
-     * Set the displayable text for this item.
-     *
-     * @param text the displayable text for this item
-     */
-    public final void setText(final String text) {
-        this.text = text;
-    }
-
     /**
      * If true, this item is expanded in the tree view.
      */
     private boolean expanded = true;
 
-    /**
-     * Get expanded value.
-     *
-     * @return if true, this item is expanded
-     */
-    public final boolean isExpanded() {
-        return expanded;
-    }
-
-    /**
-     * Set expanded value.
-     *
-     * @param expanded new value
-     */
-    public final void setExpanded(final boolean expanded) {
-        this.expanded = expanded;
-    }
-
     /**
      * If true, this item can be expanded in the tree view.
      */
     private boolean expandable = false;
 
-    /**
-     * Get expandable value.
-     *
-     * @return if true, this item is expandable
-     */
-    public final boolean isExpandable() {
-        return expandable;
-    }
-
-    /**
-     * Set expandable value.
-     *
-     * @param expandable new value
-     */
-    public final void setExpandable(final boolean expandable) {
-        this.expandable = expandable;
-    }
-
     /**
      * The vertical bars and such along the left side.
      */
     private String prefix = "";
 
     /**
-     * Get the vertical bars and such along the left side.
-     *
-     * @return the vertical bars and such along the left side
+     * Tree level.
      */
-    public final String getPrefix() {
-        return prefix;
-    }
-
-    /**
-     * Whether or not this item is last in its parent's list of children.
-     */
-    private boolean last = false;
-
-    /**
-     * Tree level.  Note package private access.
-     */
-    int level = 0;
-
-    /**
-     * If true, this item will not be drawn.
-     */
-    private boolean invisible = false;
-
-    /**
-     * Set invisible value.
-     *
-     * @param invisible new value
-     */
-    public final void setInvisible(final boolean invisible) {
-        this.invisible = invisible;
-    }
+    protected int level = 0;
 
     /**
      * True means selected.
      */
     private boolean selected = false;
 
-    /**
-     * Get selected value.
-     *
-     * @return if true, this item is selected
-     */
-    public final boolean isSelected() {
-        return selected;
-    }
-
-    /**
-     * Set selected value.
-     *
-     * @param selected new value
-     */
-    public final void setSelected(final boolean selected) {
-        this.selected = selected;
-    }
-
     /**
      * True means select-able.
      */
     private boolean selectable = true;
 
     /**
-     * Set selectable value.
-     *
-     * @param selectable new value
+     * Whether or not this item is last in its parent's list of children.
      */
-    public final void setSelectable(final boolean selectable) {
-        this.selectable = selectable;
-    }
+    private boolean last = false;
 
     /**
      * Pointer to the previous keyboard-navigable item (kbUp).  Note package
@@ -213,6 +105,10 @@ public class TTreeItem extends TWidget {
      */
     TTreeItem keyboardNext = null;
 
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Public constructor.
      *
@@ -224,107 +120,21 @@ public class TTreeItem extends TWidget {
         final boolean expanded) {
 
         super(view, 0, 0, view.getWidth() - 3, 1);
+
         this.text = text;
         this.expanded = expanded;
         this.view = view;
 
         if (view.getTreeRoot() == null) {
-            view.setTreeRoot(this, true);
-        }
-
-        view.reflowData();
-    }
-
-    /**
-     * Add a child item.
-     *
-     * @param text text for this item
-     * @return the new child item
-     */
-    public TTreeItem addChild(final String text) {
-        return addChild(text, true);
-    }
-
-    /**
-     * Add a child item.
-     *
-     * @param text text for this item
-     * @param expanded if true, have it expanded immediately
-     * @return the new child item
-     */
-    public TTreeItem addChild(final String text, final boolean expanded) {
-        TTreeItem item = new TTreeItem(view, text, expanded);
-        item.level = this.level + 1;
-        getChildren().add(item);
-        view.reflowData();
-        return item;
-    }
-
-    /**
-     * Recursively expand the tree into a linear array of items.
-     *
-     * @param prefix vertical bar of parent levels and such that is set on
-     * each child
-     * @param last if true, this is the "last" leaf node of a tree
-     * @return additional items to add to the array
-     */
-    public List<TTreeItem> expandTree(final String prefix, final boolean last) {
-        List<TTreeItem> array = new ArrayList<TTreeItem>();
-        this.last = last;
-        this.prefix = prefix;
-        array.add(this);
-
-        if ((getChildren().size() == 0) || !expanded) {
-            return array;
-        }
-
-        String newPrefix = prefix;
-        if (level > 0) {
-            if (last) {
-                newPrefix += "  ";
-            } else {
-                newPrefix += GraphicsChars.CP437[0xB3];
-                newPrefix += ' ';
-            }
-        }
-        for (int i = 0; i < getChildren().size(); i++) {
-            TTreeItem item = (TTreeItem) getChildren().get(i);
-            if (i == getChildren().size() - 1) {
-                array.addAll(item.expandTree(newPrefix, true));
-            } else {
-                array.addAll(item.expandTree(newPrefix, false));
-            }
-        }
-        return array;
-    }
-
-    /**
-     * Get the x spot for the + or - to expand/collapse.
-     *
-     * @return column of the expand/collapse button
-     */
-    private int getExpanderX() {
-        if ((level == 0) || (!expandable)) {
-            return 0;
+            view.setTreeRoot(this);
+        } else {
+            view.alignTree();
         }
-        return prefix.length() + 3;
     }
 
-    /**
-     * Recursively unselect my or my children.
-     */
-    public void unselect() {
-        if (selected == true) {
-            selected = false;
-            view.setSelected(null);
-        }
-        for (TWidget widget: getChildren()) {
-            if (widget instanceof TTreeItem) {
-                TTreeItem item = (TTreeItem) widget;
-                item.unselect();
-            }
-        }
-    }
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Handle mouse release events.
@@ -333,9 +143,13 @@ public class TTreeItem extends TWidget {
      */
     @Override
     public void onMouseUp(final TMouseEvent mouse) {
-        if ((mouse.getX() == (getExpanderX() - view.getHorizontalValue()))
+        if ((mouse.getX() == (getExpanderX() - view.getLeftColumn()))
             && (mouse.getY() == 0)
         ) {
+            if (level == 0) {
+                // Root node can't switch.
+                return;
+            }
             if (selectable) {
                 // Flip expanded flag
                 expanded = !expanded;
@@ -343,16 +157,18 @@ public class TTreeItem extends TWidget {
                     // Unselect children that became invisible
                     unselect();
                 }
+                view.setSelected(this, false);
             }
             // Let subclasses do something with this
             onExpand();
+
+            // Update the screen after any thing has expanded/contracted
+            view.alignTree();
         } else if (mouse.getY() == 0) {
-            view.setSelected(this);
+            // Do the action associated with this item.
+            view.setSelected(this, false);
             view.dispatch();
         }
-
-        // Update the screen after any thing has expanded/contracted
-        view.reflowData();
     }
 
     /**
@@ -377,6 +193,10 @@ public class TTreeItem extends TWidget {
             || keypress.equals(kbRight)
             || keypress.equals(kbSpace)
         ) {
+            if (level == 0) {
+                // Root node can't switch.
+                return;
+            }
             if (selectable) {
                 // Flip expanded flag
                 expanded = !expanded;
@@ -384,26 +204,33 @@ public class TTreeItem extends TWidget {
                     // Unselect children that became invisible
                     unselect();
                 }
-                view.setSelected(this);
+                view.setSelected(this, false);
             }
             // Let subclasses do something with this
             onExpand();
+        } else if (keypress.equals(kbEnter)) {
+            // Do the action associated with this item.
+            view.dispatch();
         } else {
             // Pass other keys (tab etc.) on to TWidget's handler.
             super.onKeypress(keypress);
         }
     }
 
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Draw this item to a window.
      */
     @Override
     public void draw() {
-        if (invisible) {
+        if ((getY() < 0) || (getY() > getParent().getHeight() - 1)) {
             return;
         }
 
-        int offset = -view.getHorizontalValue();
+        int offset = -view.getLeftColumn();
 
         CellAttributes color = getTheme().getColor("ttreeview");
         CellAttributes textColor = getTheme().getColor("ttreeview");
@@ -413,6 +240,7 @@ public class TTreeItem extends TWidget {
         if (!getParent().isAbsoluteActive()) {
             color = getTheme().getColor("ttreeview.inactive");
             textColor = getTheme().getColor("ttreeview.inactive");
+            selectedColor = getTheme().getColor("ttreeview.selected.inactive");
         }
 
         if (!selectable) {
@@ -432,6 +260,8 @@ public class TTreeItem extends TWidget {
             line += GraphicsChars.CP437[0xC4];
             if (expandable) {
                 line += "[ ] ";
+            } else {
+                line += " ";
             }
         }
         getScreen().putStringXY(offset, 0, line, color);
@@ -452,4 +282,204 @@ public class TTreeItem extends TWidget {
         }
     }
 
+    // ------------------------------------------------------------------------
+    // TTreeItem --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the parent TTreeView.
+     *
+     * @return the parent TTreeView
+     */
+    public final TTreeView getTreeView() {
+        return view;
+    }
+
+    /**
+     * Get the displayable text for this item.
+     *
+     * @return the displayable text for this item
+     */
+    public final String getText() {
+        return text;
+    }
+
+    /**
+     * Set the displayable text for this item.
+     *
+     * @param text the displayable text for this item
+     */
+    public final void setText(final String text) {
+        this.text = text;
+    }
+
+    /**
+     * Get expanded value.
+     *
+     * @return if true, this item is expanded
+     */
+    public final boolean isExpanded() {
+        return expanded;
+    }
+
+    /**
+     * Set expanded value.
+     *
+     * @param expanded new value
+     */
+    public final void setExpanded(final boolean expanded) {
+        if (level == 0) {
+            // Root node can't be unexpanded, ever.
+            this.expanded = true;
+            return;
+        }
+        if (level > 0) {
+            this.expanded = expanded;
+        }
+    }
+
+    /**
+     * Get expandable value.
+     *
+     * @return if true, this item is expandable
+     */
+    public final boolean isExpandable() {
+        return expandable;
+    }
+
+    /**
+     * Set expandable value.
+     *
+     * @param expandable new value
+     */
+    public final void setExpandable(final boolean expandable) {
+        if (level == 0) {
+            // Root node can't be unexpanded, ever.
+            this.expandable = true;
+            return;
+        }
+        if (level > 0) {
+            this.expandable = expandable;
+        }
+    }
+
+    /**
+     * Get the vertical bars and such along the left side.
+     *
+     * @return the vertical bars and such along the left side
+     */
+    public final String getPrefix() {
+        return prefix;
+    }
+
+    /**
+     * Get selected value.
+     *
+     * @return if true, this item is selected
+     */
+    public final boolean isSelected() {
+        return selected;
+    }
+
+    /**
+     * Set selected value.
+     *
+     * @param selected new value
+     */
+    public final void setSelected(final boolean selected) {
+        this.selected = selected;
+    }
+
+    /**
+     * Set selectable value.
+     *
+     * @param selectable new value
+     */
+    public final void setSelectable(final boolean selectable) {
+        this.selectable = selectable;
+    }
+
+    /**
+     * Get the length of the widest item to display.
+     *
+     * @return the maximum number of columns for this item or its children
+     */
+    public int getMaximumColumn() {
+        int max = prefix.length() + 4 + text.length();
+        for (TWidget widget: getChildren()) {
+            TTreeItem item = (TTreeItem) widget;
+            int n = item.prefix.length() + 4 + item.text.length();
+            if (n > max) {
+                max = n;
+            }
+        }
+        return max;
+    }
+
+    /**
+     * Recursively expand the tree into a linear array of items.
+     *
+     * @param prefix vertical bar of parent levels and such that is set on
+     * each child
+     * @param last if true, this is the "last" leaf node of a tree
+     * @return additional items to add to the array
+     */
+    public List<TTreeItem> expandTree(final String prefix, final boolean last) {
+        List<TTreeItem> array = new ArrayList<TTreeItem>();
+        this.last = last;
+        this.prefix = prefix;
+        array.add(this);
+
+        if ((getChildren().size() == 0) || !expanded) {
+            return array;
+        }
+
+        String newPrefix = prefix;
+        if (level > 0) {
+            if (last) {
+                newPrefix += "  ";
+            } else {
+                newPrefix += GraphicsChars.CP437[0xB3];
+                newPrefix += ' ';
+            }
+        }
+        for (int i = 0; i < getChildren().size(); i++) {
+            TTreeItem item = (TTreeItem) getChildren().get(i);
+            if (i == getChildren().size() - 1) {
+                array.addAll(item.expandTree(newPrefix, true));
+            } else {
+                array.addAll(item.expandTree(newPrefix, false));
+            }
+        }
+        return array;
+    }
+
+    /**
+     * Get the x spot for the + or - to expand/collapse.
+     *
+     * @return column of the expand/collapse button
+     */
+    private int getExpanderX() {
+        if ((level == 0) || (!expandable)) {
+            return 0;
+        }
+        return prefix.length() + 3;
+    }
+
+    /**
+     * Recursively unselect me and my children.
+     */
+    public void unselect() {
+        if (selected == true) {
+            selected = false;
+            view.setSelected(null, false);
+        }
+        for (TWidget widget: getChildren()) {
+            if (widget instanceof TTreeItem) {
+                TTreeItem item = (TTreeItem) widget;
+                item.unselect();
+            }
+        }
+    }
+
 }
diff --git a/src/jexer/ttree/TTreeView.java b/src/jexer/ttree/TTreeView.java
new file mode 100644 (file)
index 0000000..acc8924
--- /dev/null
@@ -0,0 +1,329 @@
+/*
+ * 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.ttree;
+
+import jexer.TAction;
+import jexer.TKeypress;
+import jexer.TWidget;
+import jexer.event.TKeypressEvent;
+import static jexer.TKeypress.*;
+
+/**
+ * TTreeView implements a simple tree view.
+ */
+public class TTreeView extends TWidget {
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Root of the tree.
+     */
+    private TTreeItem treeRoot;
+
+    /**
+     * Only one of my children can be selected.
+     */
+    private TTreeItem selectedItem = null;
+
+    /**
+     * The action to perform when the user selects an item.
+     */
+    private TAction action = null;
+
+    /**
+     * The top line currently visible.
+     */
+    private int topLine = 0;
+
+    /**
+     * The left column currently visible.
+     */
+    private int leftColumn = 0;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of tree view
+     * @param height height of tree view
+     */
+    public TTreeView(final TWidget parent, final int x, final int y,
+        final int width, final int height) {
+
+        this(parent, x, y, width, height, null);
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of tree view
+     * @param height height of tree view
+     * @param action action to perform when an item is selected
+     */
+    public TTreeView(final TWidget parent, final int x, final int y,
+        final int width, final int height, final TAction action) {
+
+        super(parent, x, y, width, height);
+        this.action = action;
+    }
+
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Handle keystrokes.
+     *
+     * @param keypress keystroke event
+     */
+    @Override
+    public void onKeypress(final TKeypressEvent keypress) {
+        if (keypress.equals(kbUp)) {
+            // Select the previous item
+            if (selectedItem != null) {
+                if (selectedItem.keyboardPrevious != null) {
+                    setSelected(selectedItem.keyboardPrevious, true);
+                }
+            }
+        } else if (keypress.equals(kbDown)) {
+            // Select the next item
+            if (selectedItem != null) {
+                if (selectedItem.keyboardNext != null) {
+                    setSelected(selectedItem.keyboardNext, true);
+                }
+            }
+        } else if (keypress.equals(kbPgDn)) {
+            for (int i = 0; i < getHeight() - 1; i++) {
+                onKeypress(new TKeypressEvent(TKeypress.kbDown));
+            }
+        } else if (keypress.equals(kbPgUp)) {
+            for (int i = 0; i < getHeight() - 1; i++) {
+                onKeypress(new TKeypressEvent(TKeypress.kbUp));
+            }
+        } else if (keypress.equals(kbHome)) {
+            setSelected((TTreeItem) getChildren().get(0), false);
+            setTopLine(0);
+        } else if (keypress.equals(kbEnd)) {
+            setSelected((TTreeItem) getChildren().get(getChildren().size() - 1),
+                true);
+        } else {
+            if (selectedItem != null) {
+                selectedItem.onKeypress(keypress);
+            } else {
+                // Pass other keys (tab etc.) on to TWidget's handler.
+                super.onKeypress(keypress);
+            }
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+
+    // ------------------------------------------------------------------------
+    // TTreeView --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the root of the tree.
+     *
+     * @return the root of the tree
+     */
+    public final TTreeItem getTreeRoot() {
+        return treeRoot;
+    }
+
+    /**
+     * Set the root of the tree.
+     *
+     * @param treeRoot the new root of the tree
+     */
+    public final void setTreeRoot(final TTreeItem treeRoot) {
+        this.treeRoot = treeRoot;
+        alignTree();
+    }
+
+    /**
+     * Get the tree view item that was selected.
+     *
+     * @return the selected item, or null if no item is selected
+     */
+    public final TTreeItem getSelected() {
+        return selectedItem;
+    }
+
+    /**
+     * Set the new selected tree view item.
+     *
+     * @param item new item that became selected
+     * @param centerWindow if true, move the window to put the selected into
+     * view
+     */
+    public void setSelected(final TTreeItem item, final boolean centerWindow) {
+        if (item != null) {
+            item.setSelected(true);
+        }
+        if ((selectedItem != null) && (selectedItem != item)) {
+            selectedItem.setSelected(false);
+        }
+        selectedItem = item;
+
+        if (centerWindow) {
+            int y = 0;
+            for (TWidget widget: getChildren()) {
+                if (widget == selectedItem) {
+                    break;
+                }
+                y++;
+            }
+            topLine = y - (getHeight() - 1)/2;
+            if (topLine > getChildren().size() - getHeight()) {
+                topLine = getChildren().size() - getHeight();
+            }
+            if (topLine < 0) {
+                topLine = 0;
+            }
+        }
+
+        if (selectedItem != null) {
+            activate(selectedItem);
+        }
+    }
+
+    /**
+     * Perform user selection action.
+     */
+    public void dispatch() {
+        if (action != null) {
+            action.DO();
+        }
+    }
+
+    /**
+     * Get the left column value.  0 is the leftmost column.
+     *
+     * @return the left column
+     */
+    public int getLeftColumn() {
+        return leftColumn;
+    }
+
+    /**
+     * Set the left column value.  0 is the leftmost column.
+     *
+     * @param leftColumn the new left column
+     */
+    public void setLeftColumn(final int leftColumn) {
+        this.leftColumn = leftColumn;
+    }
+
+    /**
+     * Get the top line (row) value.  0 is the topmost line.
+     *
+     * @return the top line
+     */
+    public int getTopLine() {
+        return topLine;
+    }
+
+    /**
+     * Set the top line value.  0 is the topmost line.
+     *
+     * @param topLine the new top line
+     */
+    public void setTopLine(final int topLine) {
+        this.topLine = topLine;
+    }
+
+    /**
+     * Get the total line (rows) count, based on the items that are visible
+     * and expanded.
+     *
+     * @return the line count
+     */
+    public int getTotalLineCount() {
+        if (treeRoot == null) {
+            return 0;
+        }
+        return getChildren().size();
+    }
+
+    /**
+     * Get the length of the widest item to display.
+     *
+     * @return the maximum number of columns for this item or its children
+     */
+    public int getMaximumColumn() {
+        if (treeRoot == null) {
+            return 0;
+        }
+        return treeRoot.getMaximumColumn();
+    }
+
+    /**
+     * Update the Y positions of all the children items to match the current
+     * topLine value.  Note package private access.
+     */
+    void alignTree() {
+        if (treeRoot == null) {
+            return;
+        }
+
+        // As we walk the list we also adjust next/previous pointers,
+        // resulting in a doubly-linked list but only of the expanded items.
+        TTreeItem p = null;
+
+        for (int i = 0; i < getChildren().size(); i++) {
+            TTreeItem item = (TTreeItem) getChildren().get(i);
+
+            if (p != null) {
+                item.keyboardPrevious = p;
+                p.keyboardNext = item;
+            }
+            p = item;
+
+            item.setY(i - topLine);
+            item.setWidth(getWidth());
+        }
+
+    }
+
+}
similarity index 58%
rename from src/jexer/TTreeView.java
rename to src/jexer/ttree/TTreeViewWidget.java
index b9b05bf0fd492dc0d5b83933a0736d52d7720c55..adb9a5d8e354b1974d70843e608944e0dd4b5c8b 100644 (file)
  * @author Kevin Lamonte [kevin.lamonte@gmail.com]
  * @version 1
  */
-package jexer;
-
+package jexer.ttree;
+
+import jexer.TAction;
+import jexer.THScroller;
+import jexer.TKeypress;
+import jexer.TScrollableWidget;
+import jexer.TVScroller;
+import jexer.TWidget;
 import jexer.event.TKeypressEvent;
 import jexer.event.TMouseEvent;
 import static jexer.TKeypress.*;
 
 /**
- * TTreeView implements a simple tree view.
+ * TTreeViewWidget wraps a tree view with horizontal and vertical scrollbars.
  */
-public class TTreeView extends TScrollableWidget {
-
-    /**
-     * Root of the tree.
-     */
-    private TTreeItem treeRoot;
-
-    /**
-     * Get the root of the tree.
-     *
-     * @return the root of the tree
-     */
-    public final TTreeItem getTreeRoot() {
-        return treeRoot;
-    }
-
-    /**
-     * Set the root of the tree.
-     *
-     * @param treeRoot the new root of the tree
-     */
-    public final void setTreeRoot(final TTreeItem treeRoot) {
-        this.treeRoot = treeRoot;
-    }
+public class TTreeViewWidget extends TScrollableWidget {
 
-    /**
-     * Maximum width of a single line.
-     */
-    private int maxLineWidth;
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
-     * Only one of my children can be selected.
+     * The TTreeView
      */
-    private TTreeItem selectedItem = null;
+    private TTreeView treeView;
 
     /**
      * If true, move the window to put the selected item in view.  This
@@ -77,22 +59,13 @@ public class TTreeView extends TScrollableWidget {
     private boolean centerWindow = false;
 
     /**
-     * The action to perform when the user selects an item.
-     */
-    private TAction action = null;
-
-    /**
-     * Set treeRoot.
-     *
-     * @param treeRoot ultimate root of tree
-     * @param centerWindow if true, move the window to put the root in view
+     * Maximum width of a single line.
      */
-    public void setTreeRoot(final TTreeItem treeRoot,
-        final boolean centerWindow) {
+    private int maxLineWidth;
 
-        this.treeRoot = treeRoot;
-        this.centerWindow = centerWindow;
-    }
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Public constructor.
@@ -103,7 +76,7 @@ public class TTreeView extends TScrollableWidget {
      * @param width width of tree view
      * @param height height of tree view
      */
-    public TTreeView(final TWidget parent, final int x, final int y,
+    public TTreeViewWidget(final TWidget parent, final int x, final int y,
         final int width, final int height) {
 
         this(parent, x, y, width, height, null);
@@ -119,174 +92,22 @@ public class TTreeView extends TScrollableWidget {
      * @param height height of tree view
      * @param action action to perform when an item is selected
      */
-    public TTreeView(final TWidget parent, final int x, final int y,
+    public TTreeViewWidget(final TWidget parent, final int x, final int y,
         final int width, final int height, final TAction action) {
 
         super(parent, x, y, width, height);
-        this.action = action;
+
+        treeView = new TTreeView(this, 0, 0, getWidth() - 1, getHeight() - 1,
+            action);
 
         vScroller = new TVScroller(this, getWidth() - 1, 0, getHeight() - 1);
         hScroller = new THScroller(this, 0, getHeight() - 1, getWidth() - 1);
-    }
-
-    /**
-     * Get the tree view item that was selected.
-     *
-     * @return the selected item, or null if no item is selected
-     */
-    public final TTreeItem getSelected() {
-        return selectedItem;
-    }
-
-    /**
-     * Set the new selected tree view item.
-     *
-     * @param item new item that became selected
-     */
-    public void setSelected(final TTreeItem item) {
-        if (item != null) {
-            item.setSelected(true);
-        }
-        if ((selectedItem != null) && (selectedItem != item)) {
-            selectedItem.setSelected(false);
-        }
-        selectedItem = item;
-    }
-
-    /**
-     * Perform user selection action.
-     */
-    public void dispatch() {
-        if (action != null) {
-            action.DO();
-        }
-    }
 
-    /**
-     * Resize text and scrollbars for a new width/height.
-     */
-    @Override
-    public void reflowData() {
-        int selectedRow = 0;
-        boolean foundSelectedRow = false;
-
-        if (treeRoot == null) {
-            return;
-        }
-
-        // Make each child invisible/inactive to start, expandTree() will
-        // reactivate the visible ones.
-        for (TWidget widget: getChildren()) {
-            if (widget instanceof TTreeItem) {
-                TTreeItem item = (TTreeItem) widget;
-                item.setInvisible(true);
-                item.setEnabled(false);
-                item.keyboardPrevious = null;
-                item.keyboardNext = null;
-            }
-        }
-
-        // Expand the tree into a linear list
-        getChildren().clear();
-        getChildren().addAll(treeRoot.expandTree("", true));
-
-        // Locate the selected row and maximum line width
-        for (TWidget widget: getChildren()) {
-            TTreeItem item = (TTreeItem) widget;
-
-            if (item == selectedItem) {
-                foundSelectedRow = true;
-            }
-            if (!foundSelectedRow) {
-                selectedRow++;
-            }
-
-            int lineWidth = item.getText().length()
-                + item.getPrefix().length() + 4;
-            if (lineWidth > maxLineWidth) {
-                maxLineWidth = lineWidth;
-            }
-        }
-
-        if ((centerWindow) && (foundSelectedRow)) {
-            if ((selectedRow < getVerticalValue())
-                || (selectedRow > getVerticalValue() + getHeight() - 2)
-            ) {
-                setVerticalValue(selectedRow);
-                centerWindow = false;
-            }
-        }
-        updatePositions();
-
-        // Rescale the scroll bars
-        setBottomValue(getChildren().size() - getHeight() + 1);
-        if (getBottomValue() < 0) {
-            setBottomValue(0);
-        }
-        if (getVerticalValue() > getBottomValue()) {
-            setVerticalValue(getBottomValue());
-        }
-        setRightValue(maxLineWidth - getWidth() + 3);
-        if (getRightValue() < 0) {
-            setRightValue(0);
-        }
-        if (getHorizontalValue() > getRightValue()) {
-            setHorizontalValue(getRightValue());
-        }
-        getChildren().add(hScroller);
-        getChildren().add(vScroller);
     }
 
-    /**
-     * Update the Y positions of all the children items.
-     */
-    private void updatePositions() {
-        if (treeRoot == null) {
-            return;
-        }
-
-        int begin = getVerticalValue();
-        int topY = 0;
-
-        // As we walk the list we also adjust next/previous pointers,
-        // resulting in a doubly-linked list but only of the expanded items.
-        TTreeItem p = null;
-
-        for (int i = 0; i < getChildren().size(); i++) {
-            if (!(getChildren().get(i) instanceof TTreeItem)) {
-                // Skip the scrollbars
-                continue;
-            }
-            TTreeItem item = (TTreeItem) getChildren().get(i);
-
-            if (p != null) {
-                item.keyboardPrevious = p;
-                p.keyboardNext = item;
-            }
-            p = item;
-
-            if (i < begin) {
-                // Render invisible
-                item.setEnabled(false);
-                item.setInvisible(true);
-                continue;
-            }
-
-            if (topY >= getHeight() - 1) {
-                // Render invisible
-                item.setEnabled(false);
-                item.setInvisible(true);
-                continue;
-            }
-
-            item.setY(topY);
-            item.setEnabled(true);
-            item.setInvisible(false);
-            item.setWidth(getWidth() - 1);
-            topY++;
-        }
-
-    }
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
      * Handle mouse press events.
@@ -300,11 +121,13 @@ public class TTreeView extends TScrollableWidget {
         } else if (mouse.isMouseWheelDown()) {
             verticalIncrement();
         } else {
-            // Pass to children
+            // Pass to the TreeView or scrollbars
             super.onMouseDown(mouse);
         }
 
-        // Update the screen after the scrollbars have moved
+        // Update the view to reflect the new scrollbar positions
+        treeView.setTopLine(getVerticalValue());
+        treeView.setLeftColumn(getHorizontalValue());
         reflowData();
     }
 
@@ -315,10 +138,28 @@ public class TTreeView extends TScrollableWidget {
      */
     @Override
     public void onMouseUp(final TMouseEvent mouse) {
-        // Pass to children
-        super.onMouseDown(mouse);
+        // Pass to the TreeView or scrollbars
+        super.onMouseUp(mouse);
+
+        // Update the view to reflect the new scrollbar positions
+        treeView.setTopLine(getVerticalValue());
+        treeView.setLeftColumn(getHorizontalValue());
+        reflowData();
+    }
+
+    /**
+     * Handle mouse motion events.
+     *
+     * @param mouse mouse motion event
+     */
+    @Override
+    public void onMouseMotion(final TMouseEvent mouse) {
+        // Pass to the TreeView or scrollbars
+        super.onMouseMotion(mouse);
 
-        // Update the screen after any thing has expanded/contracted
+        // Update the view to reflect the new scrollbar positions
+        treeView.setTopLine(getVerticalValue());
+        treeView.setLeftColumn(getHorizontalValue());
         reflowData();
     }
 
@@ -359,36 +200,29 @@ public class TTreeView extends TScrollableWidget {
             || keypress.equals(kbAltPgDn)
         ) {
             bigVerticalIncrement();
-        } else if (keypress.equals(kbHome)) {
-            toTop();
-        } else if (keypress.equals(kbEnd)) {
-            toBottom();
-        } else if (keypress.equals(kbEnter)) {
-            if (selectedItem != null) {
-                dispatch();
+        } else if (keypress.equals(kbPgDn)) {
+            for (int i = 0; i < getHeight() - 2; i++) {
+                treeView.onKeypress(new TKeypressEvent(TKeypress.kbDown));
             }
-        } else if (keypress.equals(kbUp)) {
-            // Select the previous item
-            if (selectedItem != null) {
-                TTreeItem oldItem = selectedItem;
-                if (selectedItem.keyboardPrevious != null) {
-                    setSelected(selectedItem.keyboardPrevious);
-                    if (oldItem.getY() == 0) {
-                        verticalDecrement();
-                    }
-                }
-            }
-        } else if (keypress.equals(kbDown)) {
-            // Select the next item
-            if (selectedItem != null) {
-                TTreeItem oldItem = selectedItem;
-                if (selectedItem.keyboardNext != null) {
-                    setSelected(selectedItem.keyboardNext);
-                    if (oldItem.getY() == getHeight() - 2) {
-                        verticalIncrement();
-                    }
-                }
+            reflowData();
+            return;
+        } else if (keypress.equals(kbPgUp)) {
+            for (int i = 0; i < getHeight() - 2; i++) {
+                treeView.onKeypress(new TKeypressEvent(TKeypress.kbUp));
             }
+            reflowData();
+            return;
+        } else if (keypress.equals(kbHome)) {
+            treeView.setSelected((TTreeItem) treeView.getChildren().get(0),
+                false);
+            treeView.setTopLine(0);
+            reflowData();
+            return;
+        } else if (keypress.equals(kbEnd)) {
+            treeView.setSelected((TTreeItem)  treeView.getChildren().get(
+                treeView.getChildren().size() - 1), true);
+            reflowData();
+            return;
         } else if (keypress.equals(kbTab)) {
             getParent().switchWidget(true);
             return;
@@ -396,17 +230,157 @@ public class TTreeView extends TScrollableWidget {
                 || keypress.equals(kbBackTab)) {
             getParent().switchWidget(false);
             return;
-        } else if (selectedItem != null) {
-            // Give the TTreeItem a chance to handle arrow keys
-            selectedItem.onKeypress(keypress);
         } else {
-            // Pass other keys (tab etc.) on to TWidget's handler.
-            super.onKeypress(keypress);
+            treeView.onKeypress(keypress);
+
+            // Update the scrollbars to reflect the new data position
+            reflowData();
             return;
         }
 
-        // Update the screen after any thing has expanded/contracted
+        // Update the view to reflect the new scrollbar position
+        treeView.setTopLine(getVerticalValue());
+        treeView.setLeftColumn(getHorizontalValue());
         reflowData();
     }
 
+    // ------------------------------------------------------------------------
+    // TScrollableWidget ------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Resize text and scrollbars for a new width/height.
+     */
+    @Override
+    public void reflowData() {
+        int selectedRow = 0;
+        boolean foundSelectedRow = false;
+
+        // Reset the keyboard list, expandTree() will recreate it.
+        for (TWidget widget: treeView.getChildren()) {
+            TTreeItem item = (TTreeItem) widget;
+            item.keyboardPrevious = null;
+            item.keyboardNext = null;
+        }
+
+        // Expand the tree into a linear list
+        treeView.getChildren().clear();
+        treeView.getChildren().addAll(treeView.getTreeRoot().expandTree("",
+                true));
+
+        // Locate the selected row and maximum line width
+        for (TWidget widget: treeView.getChildren()) {
+            TTreeItem item = (TTreeItem) widget;
+
+            if (item == treeView.getSelected()) {
+                foundSelectedRow = true;
+            }
+            if (!foundSelectedRow) {
+                selectedRow++;
+            }
+
+            int lineWidth = item.getText().length()
+                + item.getPrefix().length() + 4;
+            if (lineWidth > maxLineWidth) {
+                maxLineWidth = lineWidth;
+            }
+        }
+
+        if ((centerWindow) && (foundSelectedRow)) {
+            if ((selectedRow < getVerticalValue())
+                || (selectedRow > getVerticalValue() + getHeight() - 2)
+            ) {
+                treeView.setTopLine(selectedRow);
+                centerWindow = false;
+            }
+        }
+        treeView.alignTree();
+
+        // Rescale the scroll bars
+        setVerticalValue(treeView.getTopLine());
+        setBottomValue(treeView.getTotalLineCount() - (getHeight() - 1));
+        if (getBottomValue() < getTopValue()) {
+            setBottomValue(getTopValue());
+        }
+        if (getVerticalValue() > getBottomValue()) {
+            setVerticalValue(getBottomValue());
+        }
+        setRightValue(maxLineWidth - 2);
+        if (getHorizontalValue() > getRightValue()) {
+            setHorizontalValue(getRightValue());
+        }
+
+    }
+
+    // ------------------------------------------------------------------------
+    // TTreeView --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the underlying TTreeView.
+     *
+     * @return the TTreeView
+     */
+    public TTreeView getTreeView() {
+        return treeView;
+    }
+
+    /**
+     * Get the root of the tree.
+     *
+     * @return the root of the tree
+     */
+    public final TTreeItem getTreeRoot() {
+        return treeView.getTreeRoot();
+    }
+
+    /**
+     * Set the root of the tree.
+     *
+     * @param treeRoot the new root of the tree
+     */
+    public final void setTreeRoot(final TTreeItem treeRoot) {
+        treeView.setTreeRoot(treeRoot);
+    }
+
+    /**
+     * Set treeRoot.
+     *
+     * @param treeRoot ultimate root of tree
+     * @param centerWindow if true, move the window to put the root in view
+     */
+    public void setTreeRoot(final TTreeItem treeRoot,
+        final boolean centerWindow) {
+
+        treeView.setTreeRoot(treeRoot);
+        this.centerWindow = centerWindow;
+    }
+
+    /**
+     * Get the tree view item that was selected.
+     *
+     * @return the selected item, or null if no item is selected
+     */
+    public final TTreeItem getSelected() {
+        return treeView.getSelected();
+    }
+
+    /**
+     * Set the new selected tree view item.
+     *
+     * @param item new item that became selected
+     * @param centerWindow if true, move the window to put the selected into
+     * view
+     */
+    public void setSelected(final TTreeItem item, final boolean centerWindow) {
+        treeView.setSelected(item, centerWindow);
+    }
+
+    /**
+     * Perform user selection action.
+     */
+    public void dispatch() {
+        treeView.dispatch();
+    }
+
 }
diff --git a/src/jexer/ttree/TTreeViewWindow.java b/src/jexer/ttree/TTreeViewWindow.java
new file mode 100644 (file)
index 0000000..2adf616
--- /dev/null
@@ -0,0 +1,400 @@
+/*
+ * 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.ttree;
+
+import jexer.TAction;
+import jexer.TApplication;
+import jexer.THScroller;
+import jexer.TScrollableWindow;
+import jexer.TVScroller;
+import jexer.TWidget;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
+import jexer.event.TResizeEvent;
+import static jexer.TKeypress.*;
+
+/**
+ * TTreeViewWindow wraps a tree view with horizontal and vertical scrollbars
+ * in a standalone window.
+ */
+public class TTreeViewWindow extends TScrollableWindow {
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The TTreeView
+     */
+    private TTreeView treeView;
+
+    /**
+     * If true, move the window to put the selected item in view.  This
+     * normally only happens once after setting treeRoot.
+     */
+    private boolean centerWindow = false;
+
+    /**
+     * Maximum width of a single line.
+     */
+    private int maxLineWidth;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param parent the main application
+     * @param title the window title
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of tree view
+     * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
+     * @param height height of tree view
+     */
+    public TTreeViewWindow(final TApplication parent, final String title,
+        final int x, final int y, final int width, final int height,
+        final int flags) {
+
+        this(parent, title, x, y, width, height, flags, null);
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param parent the main application
+     * @param title the window title
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of tree view
+     * @param height height of tree view
+     * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
+     * @param action action to perform when an item is selected
+     */
+    public TTreeViewWindow(final TApplication parent, final String title,
+        final int x, final int y, final int width, final int height,
+        final int flags, final TAction action) {
+
+        super(parent, title, x, y, width, height, flags);
+
+        treeView = new TTreeView(this, 0, 0, getWidth() - 2, getHeight() - 2,
+            action);
+
+        hScroller = new THScroller(this, 17, getHeight() - 2, getWidth() - 20);
+        vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
+
+        /*
+        System.err.println("TTreeViewWindow()");
+        for (TWidget w: getChildren()) {
+            System.err.println("    " + w + " " + w.isActive());
+        }
+        */
+    }
+
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Handle mouse press events.
+     *
+     * @param mouse mouse button press event
+     */
+    @Override
+    public void onMouseDown(final TMouseEvent mouse) {
+        if (mouse.isMouseWheelUp()) {
+            verticalDecrement();
+        } else if (mouse.isMouseWheelDown()) {
+            verticalIncrement();
+        } else {
+            // Pass to the TreeView or scrollbars
+            super.onMouseDown(mouse);
+        }
+
+        // Update the view to reflect the new scrollbar positions
+        treeView.setTopLine(getVerticalValue());
+        treeView.setLeftColumn(getHorizontalValue());
+        reflowData();
+    }
+
+    /**
+     * Handle mouse release events.
+     *
+     * @param mouse mouse button release event
+     */
+    @Override
+    public void onMouseUp(final TMouseEvent mouse) {
+        // Pass to the TreeView or scrollbars
+        super.onMouseUp(mouse);
+
+        // Update the view to reflect the new scrollbar positions
+        treeView.setTopLine(getVerticalValue());
+        treeView.setLeftColumn(getHorizontalValue());
+        reflowData();
+    }
+
+    /**
+     * Handle mouse motion events.
+     *
+     * @param mouse mouse motion event
+     */
+    @Override
+    public void onMouseMotion(final TMouseEvent mouse) {
+        // Pass to the TreeView or scrollbars
+        super.onMouseMotion(mouse);
+
+        // Update the view to reflect the new scrollbar positions
+        treeView.setTopLine(getVerticalValue());
+        treeView.setLeftColumn(getHorizontalValue());
+        reflowData();
+    }
+
+    /**
+     * Handle keystrokes.
+     *
+     * @param keypress keystroke event
+     */
+    @Override
+    public void onKeypress(final TKeypressEvent keypress) {
+        if (inKeyboardResize) {
+            // Let TWindow do its job.
+            super.onKeypress(keypress);
+            return;
+        }
+
+        if (keypress.equals(kbShiftLeft)
+            || keypress.equals(kbCtrlLeft)
+            || keypress.equals(kbAltLeft)
+        ) {
+            horizontalDecrement();
+        } else if (keypress.equals(kbShiftRight)
+            || keypress.equals(kbCtrlRight)
+            || keypress.equals(kbAltRight)
+        ) {
+            horizontalIncrement();
+        } else if (keypress.equals(kbShiftUp)
+            || keypress.equals(kbCtrlUp)
+            || keypress.equals(kbAltUp)
+        ) {
+            verticalDecrement();
+        } else if (keypress.equals(kbShiftDown)
+            || keypress.equals(kbCtrlDown)
+            || keypress.equals(kbAltDown)
+        ) {
+            verticalIncrement();
+        } else if (keypress.equals(kbShiftPgUp)
+            || keypress.equals(kbCtrlPgUp)
+            || keypress.equals(kbAltPgUp)
+        ) {
+            bigVerticalDecrement();
+        } else if (keypress.equals(kbShiftPgDn)
+            || keypress.equals(kbCtrlPgDn)
+            || keypress.equals(kbAltPgDn)
+        ) {
+            bigVerticalIncrement();
+        } else {
+            treeView.onKeypress(keypress);
+
+            // Update the scrollbars to reflect the new data position
+            reflowData();
+            return;
+        }
+
+        // Update the view to reflect the new scrollbar position
+        treeView.setTopLine(getVerticalValue());
+        treeView.setLeftColumn(getHorizontalValue());
+        reflowData();
+    }
+
+    // ------------------------------------------------------------------------
+    // TScrollableWindow ------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Handle window/screen resize events.
+     *
+     * @param resize resize event
+     */
+    @Override
+    public void onResize(final TResizeEvent resize) {
+        if (resize.getType() == TResizeEvent.Type.WIDGET) {
+            // Resize the treeView field.
+            TResizeEvent treeSize = new TResizeEvent(TResizeEvent.Type.WIDGET,
+                resize.getWidth() - 2, resize.getHeight() - 2);
+            treeView.onResize(treeSize);
+
+            // Have TScrollableWindow handle the scrollbars.
+            super.onResize(resize);
+
+            // Now re-center the treeView field.
+            if (treeView.getSelected() != null) {
+                treeView.setSelected(treeView.getSelected(), true);
+            }
+            reflowData();
+            return;
+        }
+    }
+
+    /**
+     * Resize text and scrollbars for a new width/height.
+     */
+    @Override
+    public void reflowData() {
+        int selectedRow = 0;
+        boolean foundSelectedRow = false;
+
+        // Reset the keyboard list, expandTree() will recreate it.
+        for (TWidget widget: treeView.getChildren()) {
+            TTreeItem item = (TTreeItem) widget;
+            item.keyboardPrevious = null;
+            item.keyboardNext = null;
+        }
+
+        // Expand the tree into a linear list
+        treeView.getChildren().clear();
+        treeView.getChildren().addAll(treeView.getTreeRoot().expandTree("",
+                true));
+
+        // Locate the selected row and maximum line width
+        for (TWidget widget: treeView.getChildren()) {
+            TTreeItem item = (TTreeItem) widget;
+
+            if (item == treeView.getSelected()) {
+                foundSelectedRow = true;
+            }
+            if (!foundSelectedRow) {
+                selectedRow++;
+            }
+
+            int lineWidth = item.getText().length()
+                + item.getPrefix().length() + 4;
+            if (lineWidth > maxLineWidth) {
+                maxLineWidth = lineWidth;
+            }
+        }
+
+        if ((centerWindow) && (foundSelectedRow)) {
+            if ((selectedRow < getVerticalValue())
+                || (selectedRow > getVerticalValue() + getHeight() - 3)
+            ) {
+                treeView.setTopLine(selectedRow);
+                centerWindow = false;
+            }
+        }
+        treeView.alignTree();
+
+        // Rescale the scroll bars
+        setVerticalValue(treeView.getTopLine());
+        setBottomValue(treeView.getTotalLineCount() - (getHeight() - 2));
+        if (getBottomValue() < getTopValue()) {
+            setBottomValue(getTopValue());
+        }
+        if (getVerticalValue() > getBottomValue()) {
+            setVerticalValue(getBottomValue());
+        }
+        setRightValue(maxLineWidth - 4);
+        if (getHorizontalValue() > getRightValue()) {
+            setHorizontalValue(getRightValue());
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // TTreeView --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the underlying TTreeView.
+     *
+     * @return the TTreeView
+     */
+    public TTreeView getTreeView() {
+        return treeView;
+    }
+
+    /**
+     * Get the root of the tree.
+     *
+     * @return the root of the tree
+     */
+    public final TTreeItem getTreeRoot() {
+        return treeView.getTreeRoot();
+    }
+
+    /**
+     * Set the root of the tree.
+     *
+     * @param treeRoot the new root of the tree
+     */
+    public final void setTreeRoot(final TTreeItem treeRoot) {
+        treeView.setTreeRoot(treeRoot);
+    }
+
+    /**
+     * Set treeRoot.
+     *
+     * @param treeRoot ultimate root of tree
+     * @param centerWindow if true, move the window to put the root in view
+     */
+    public void setTreeRoot(final TTreeItem treeRoot,
+        final boolean centerWindow) {
+
+        treeView.setTreeRoot(treeRoot);
+        this.centerWindow = centerWindow;
+    }
+
+    /**
+     * Get the tree view item that was selected.
+     *
+     * @return the selected item, or null if no item is selected
+     */
+    public final TTreeItem getSelected() {
+        return treeView.getSelected();
+    }
+
+    /**
+     * Set the new selected tree view item.
+     *
+     * @param item new item that became selected
+     * @param centerWindow if true, move the window to put the selected into
+     * view
+     */
+    public void setSelected(final TTreeItem item, final boolean centerWindow) {
+        treeView.setSelected(item, centerWindow);
+    }
+
+    /**
+     * Perform user selection action.
+     */
+    public void dispatch() {
+        treeView.dispatch();
+    }
+
+}
diff --git a/src/jexer/ttree/package-info.java b/src/jexer/ttree/package-info.java
new file mode 100644 (file)
index 0000000..72dc8da
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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
+ */
+
+/**
+ * TTreeView and supporting classes.
+ */
+package jexer.ttree;