TField cut/paste working
[nikiroo-utils.git] / src / jexer / TApplication.java
index 4c317f304d6bb82c6fc39684ce17be4ffc8b53ea..13cd07958ee3a61c9b36e2a76cf0f2560e48920a 100644 (file)
@@ -47,7 +47,9 @@ import java.util.ResourceBundle;
 
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
 import jexer.bits.ColorTheme;
+import jexer.bits.StringUtils;
 import jexer.event.TCommandEvent;
 import jexer.event.TInputEvent;
 import jexer.event.TKeypressEvent;
@@ -62,6 +64,7 @@ import jexer.backend.ECMA48Backend;
 import jexer.backend.TWindowBackend;
 import jexer.menu.TMenu;
 import jexer.menu.TMenuItem;
+import jexer.menu.TSubMenu;
 import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
@@ -131,6 +134,11 @@ public class TApplication implements Runnable {
      */
     private volatile WidgetEventHandler secondaryEventHandler;
 
+    /**
+     * The screen handler thread.
+     */
+    private volatile ScreenHandler screenHandler;
+
     /**
      * The widget receiving events from the secondary event handler thread.
      */
@@ -141,6 +149,11 @@ public class TApplication implements Runnable {
      */
     private Backend backend;
 
+    /**
+     * The clipboard for copy and paste.
+     */
+    private Clipboard clipboard = new Clipboard();
+
     /**
      * Actual mouse coordinate X.
      */
@@ -263,7 +276,7 @@ public class TApplication implements Runnable {
      * constant.  Someday it would be nice to have a multi-line menu or
      * toolbars.
      */
-    private static final int desktopTop = 1;
+    private int desktopTop = 1;
 
     /**
      * Y coordinate of the bottom edge of the desktop.
@@ -283,15 +296,71 @@ public class TApplication implements Runnable {
     private boolean focusFollowsMouse = false;
 
     /**
-     * The images that might be displayed.  Note package private access.
+     * If true, display a text-based mouse cursor.
+     */
+    private boolean textMouse = true;
+
+    /**
+     * If true, hide the mouse after typing a keystroke.
+     */
+    private boolean hideMouseWhenTyping = false;
+
+    /**
+     * If true, the mouse should not be displayed because a keystroke was
+     * typed.
+     */
+    private boolean typingHidMouse = false;
+
+    /**
+     * If true, hide the status bar.
+     */
+    private boolean hideStatusBar = false;
+
+    /**
+     * If true, hide the menu bar.
      */
-    private List<TImage> images;
+    private boolean hideMenuBar = false;
 
     /**
      * The list of commands to run before the next I/O check.
      */
     private List<Runnable> invokeLaters = new LinkedList<Runnable>();
 
+    /**
+     * The last time the screen was resized.
+     */
+    private long screenResizeTime = 0;
+
+    /**
+     * If true, screen selection is a rectangle.
+     */
+    private boolean screenSelectionRectangle = false;
+
+    /**
+     * If true, the mouse is dragging a screen selection.
+     */
+    private boolean inScreenSelection = false;
+
+    /**
+     * Screen selection starting X.
+     */
+    private int screenSelectionX0;
+
+    /**
+     * Screen selection starting Y.
+     */
+    private int screenSelectionY0;
+
+    /**
+     * Screen selection ending X.
+     */
+    private int screenSelectionX1;
+
+    /**
+     * Screen selection ending Y.
+     */
+    private int screenSelectionY1;
+
     /**
      * WidgetEventHandler is the main event consumer loop.  There are at most
      * two such threads in existence: the primary for normal case and a
@@ -432,9 +501,9 @@ public class TApplication implements Runnable {
                         // resumes working on the primary.
                         application.secondaryEventHandler = null;
 
-                        // DO NOT UNLOCK.  Primary thread just came back from
-                        // primaryHandleEvent() and will unlock in the else
-                        // block below.  Just wake it up.
+                        // We are ready to exit, wake up the primary thread.
+                        // Remember that it is currently sleeping inside its
+                        // primaryHandleEvent().
                         synchronized (application.primaryEventHandler) {
                             application.primaryEventHandler.notify();
                         }
@@ -454,6 +523,94 @@ public class TApplication implements Runnable {
         }
     }
 
+    /**
+     * ScreenHandler pushes screen updates to the physical device.
+     */
+    private class ScreenHandler implements Runnable {
+        /**
+         * The main application.
+         */
+        private TApplication application;
+
+        /**
+         * The dirty flag.
+         */
+        private boolean dirty = false;
+
+        /**
+         * Public constructor.
+         *
+         * @param application the main application
+         */
+        public ScreenHandler(final TApplication application) {
+            this.application = application;
+        }
+
+        /**
+         * The screen update loop.
+         */
+        public void run() {
+            // Wrap everything in a try, so that if we go belly up we can let
+            // the user have their terminal back.
+            try {
+                runImpl();
+            } catch (Throwable t) {
+                this.application.restoreConsole();
+                t.printStackTrace();
+                this.application.exit();
+            }
+        }
+
+        /**
+         * The update loop.
+         */
+        private void runImpl() {
+
+            // Loop forever
+            while (!application.quit) {
+
+                // Wait until application notifies me
+                while (!application.quit) {
+                    try {
+                        synchronized (this) {
+                            if (dirty) {
+                                dirty = false;
+                                break;
+                            }
+
+                            // Always check within 50 milliseconds.
+                            this.wait(50);
+                        }
+                    } catch (InterruptedException e) {
+                        // SQUASH
+                    }
+                } // while (!application.quit)
+
+                 // Flush the screen contents
+                if (debugThreads) {
+                    System.err.printf("%d %s backend.flushScreen()\n",
+                        System.currentTimeMillis(), Thread.currentThread());
+                }
+                synchronized (getScreen()) {
+                    backend.flushScreen();
+                }
+            } // while (true) (main runnable loop)
+
+            // Shutdown the user I/O thread(s)
+            backend.shutdown();
+        }
+
+        /**
+         * Set the dirty flag.
+         */
+        public void setDirty() {
+            synchronized (this) {
+                dirty = true;
+            }
+        }
+
+    }
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -594,8 +751,32 @@ public class TApplication implements Runnable {
      * Finish construction once the backend is set.
      */
     private void TApplicationImpl() {
+        // Text block mouse option
+        if (System.getProperty("jexer.textMouse", "true").equals("false")) {
+            textMouse = false;
+        }
+
+        // Hide mouse when typing option
+        if (System.getProperty("jexer.hideMouseWhenTyping",
+                "false").equals("true")) {
+
+            hideMouseWhenTyping = true;
+        }
+
+        // Hide status bar option
+        if (System.getProperty("jexer.hideStatusBar",
+                "false").equals("true")) {
+            hideStatusBar = true;
+        }
+
+        // Hide menu bar option
+        if (System.getProperty("jexer.hideMenuBar", "false").equals("true")) {
+            hideMenuBar = true;
+        }
+
         theme           = new ColorTheme();
-        desktopBottom   = getScreen().getHeight() - 1;
+        desktopTop      = (hideMenuBar ? 0 : 1);
+        desktopBottom   = getScreen().getHeight() - 1 + (hideStatusBar ? 1 : 0);
         fillEventQueue  = new LinkedList<TInputEvent>();
         drainEventQueue = new LinkedList<TInputEvent>();
         windows         = new LinkedList<TWindow>();
@@ -605,7 +786,6 @@ public class TApplication implements Runnable {
         accelerators    = new HashMap<TKeypress, TMenuItem>();
         menuItems       = new LinkedList<TMenuItem>();
         desktop         = new TDesktop(this);
-        images          = new LinkedList<TImage>();
 
         // Special case: the Swing backend needs to have a timer to drive its
         // blink state.
@@ -628,6 +808,7 @@ public class TApplication implements Runnable {
                 );
             }
         }
+
     }
 
     // ------------------------------------------------------------------------
@@ -638,6 +819,12 @@ public class TApplication implements Runnable {
      * Run this application until it exits.
      */
     public void run() {
+        // System.err.println("*** TApplication.run() begins ***");
+
+        // Start the screen updater thread
+        screenHandler = new ScreenHandler(this);
+        (new Thread(screenHandler)).start();
+
         // Start the main consumer thread
         primaryEventHandler = new WidgetEventHandler(this, true);
         (new Thread(primaryEventHandler)).start();
@@ -714,13 +901,20 @@ public class TApplication implements Runnable {
             }
         }
 
-        // Shutdown the user I/O thread(s)
-        backend.shutdown();
-
         // Close all the windows.  This gives them an opportunity to release
         // resources.
         closeAllWindows();
 
+        // Close the desktop.
+        if (desktop != null) {
+            setDesktop(null);
+        }
+
+        // Give the overarching application an opportunity to release
+        // resources.
+        onExit();
+
+        // System.err.println("*** TApplication.run() exits ***");
     }
 
     // ------------------------------------------------------------------------
@@ -764,7 +958,7 @@ public class TApplication implements Runnable {
             return true;
         }
 
-        if (command.equals(cmMenu)) {
+        if (command.equals(cmMenu) && (hideMenuBar == false)) {
             if (!modalWindowActive() && (activeMenu == null)) {
                 if (menus.size() > 0) {
                     menus.get(0).setActive(true);
@@ -827,10 +1021,28 @@ public class TApplication implements Runnable {
             openImage();
             return true;
         }
-        if (menu.getId() == TMenu.MID_CHANGE_FONT) {
+        if (menu.getId() == TMenu.MID_SCREEN_OPTIONS) {
             new TFontChooserWindow(this);
             return true;
         }
+
+        if (menu.getId() == TMenu.MID_CUT) {
+            postMenuEvent(new TCommandEvent(cmCut));
+            return true;
+        }
+        if (menu.getId() == TMenu.MID_COPY) {
+            postMenuEvent(new TCommandEvent(cmCopy));
+            return true;
+        }
+        if (menu.getId() == TMenu.MID_PASTE) {
+            postMenuEvent(new TCommandEvent(cmPaste));
+            return true;
+        }
+        if (menu.getId() == TMenu.MID_CLEAR) {
+            postMenuEvent(new TCommandEvent(cmClear));
+            return true;
+        }
+
         return false;
     }
 
@@ -849,6 +1061,7 @@ public class TApplication implements Runnable {
             && !keypress.getKey().isCtrl()
             && (activeMenu == null)
             && !modalWindowActive()
+            && (hideMenuBar == false)
         ) {
 
             assert (subMenus.size() == 0);
@@ -876,6 +1089,47 @@ public class TApplication implements Runnable {
                 Thread.currentThread() + " finishEventProcessing()\n");
         }
 
+        // See if we need to enable/disable the edit menu.
+        EditMenuUser widget = null;
+        if (activeMenu == null) {
+            if (activeWindow != null) {
+                if (activeWindow.getActiveChild() instanceof EditMenuUser) {
+                    widget = (EditMenuUser) activeWindow.getActiveChild();
+                }
+            } else if (desktop != null) {
+                if (desktop.getActiveChild() instanceof EditMenuUser) {
+                    widget = (EditMenuUser) desktop.getActiveChild();
+                }
+            }
+            if (widget == null) {
+                disableMenuItem(TMenu.MID_CUT);
+                disableMenuItem(TMenu.MID_COPY);
+                disableMenuItem(TMenu.MID_PASTE);
+                disableMenuItem(TMenu.MID_CLEAR);
+            } else {
+                if (widget.isEditMenuCut()) {
+                    enableMenuItem(TMenu.MID_CUT);
+                } else {
+                    disableMenuItem(TMenu.MID_CUT);
+                }
+                if (widget.isEditMenuCopy()) {
+                    enableMenuItem(TMenu.MID_COPY);
+                } else {
+                    disableMenuItem(TMenu.MID_COPY);
+                }
+                if (widget.isEditMenuPaste()) {
+                    enableMenuItem(TMenu.MID_PASTE);
+                } else {
+                    disableMenuItem(TMenu.MID_PASTE);
+                }
+                if (widget.isEditMenuClear()) {
+                    enableMenuItem(TMenu.MID_CLEAR);
+                } else {
+                    disableMenuItem(TMenu.MID_CLEAR);
+                }
+            }
+        }
+
         // Process timers and call doIdle()'s
         doIdle();
 
@@ -884,6 +1138,9 @@ public class TApplication implements Runnable {
             drawAll();
         }
 
+        // Wake up the screen repainter
+        wakeScreenHandler();
+
         if (debugThreads) {
             System.err.printf(System.currentTimeMillis() + " " +
                 Thread.currentThread() + " finishEventProcessing() END\n");
@@ -914,7 +1171,7 @@ public class TApplication implements Runnable {
         // Abort everything
         if (event instanceof TCommandEvent) {
             TCommandEvent command = (TCommandEvent) event;
-            if (command.getCmd().equals(cmAbort)) {
+            if (command.equals(cmAbort)) {
                 exit();
                 return;
             }
@@ -925,17 +1182,27 @@ public class TApplication implements Runnable {
             if (event instanceof TResizeEvent) {
                 TResizeEvent resize = (TResizeEvent) event;
                 synchronized (getScreen()) {
-                    getScreen().setDimensions(resize.getWidth(),
-                        resize.getHeight());
+                    if ((System.currentTimeMillis() - screenResizeTime >= 15)
+                        || (resize.getWidth() < getScreen().getWidth())
+                        || (resize.getHeight() < getScreen().getHeight())
+                    ) {
+                        getScreen().setDimensions(resize.getWidth(),
+                            resize.getHeight());
+                        screenResizeTime = System.currentTimeMillis();
+                    }
                     desktopBottom = getScreen().getHeight() - 1;
+                    if (hideStatusBar) {
+                        desktopBottom++;
+                    }
                     mouseX = 0;
                     mouseY = 0;
                     oldMouseX = 0;
                     oldMouseY = 0;
                 }
                 if (desktop != null) {
-                    desktop.setDimensions(0, 0, resize.getWidth(),
-                        resize.getHeight() - 1);
+                    desktop.setDimensions(0, desktopTop, resize.getWidth(),
+                        (desktopBottom - desktopTop));
+                    desktop.onResize(resize);
                 }
 
                 // Change menu edges if needed.
@@ -974,9 +1241,39 @@ public class TApplication implements Runnable {
 
         // Special application-wide events -----------------------------------
 
+        if (event instanceof TKeypressEvent) {
+            if (hideMouseWhenTyping) {
+                typingHidMouse = true;
+            }
+        }
+
         // Peek at the mouse position
         if (event instanceof TMouseEvent) {
+            typingHidMouse = false;
+
             TMouseEvent mouse = (TMouseEvent) event;
+            if (mouse.isMouse1() && (mouse.isShift() || mouse.isCtrl())) {
+                // Screen selection.
+                if (inScreenSelection) {
+                    screenSelectionX1 = mouse.getX();
+                    screenSelectionY1 = mouse.getY();
+                } else {
+                    inScreenSelection = true;
+                    screenSelectionX0 = mouse.getX();
+                    screenSelectionY0 = mouse.getY();
+                    screenSelectionX1 = mouse.getX();
+                    screenSelectionY1 = mouse.getY();
+                    screenSelectionRectangle = mouse.isCtrl();
+                }
+            } else {
+                if (inScreenSelection) {
+                    getScreen().copySelection(clipboard, screenSelectionX0,
+                        screenSelectionY0, screenSelectionX1, screenSelectionY1,
+                        screenSelectionRectangle);
+                }
+                inScreenSelection = false;
+            }
+
             if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
                 oldMouseX = mouseX;
                 oldMouseY = mouseY;
@@ -997,7 +1294,8 @@ public class TApplication implements Runnable {
                             mouse.getAbsoluteX(), mouse.getAbsoluteY(),
                             mouse.isMouse1(), mouse.isMouse2(),
                             mouse.isMouse3(),
-                            mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+                            mouse.isMouseWheelUp(), mouse.isMouseWheelDown(),
+                            mouse.isAlt(), mouse.isCtrl(), mouse.isShift());
 
                     } else {
                         // The first click of a potential double-click.
@@ -1121,6 +1419,8 @@ public class TApplication implements Runnable {
                 }
             } else if (event instanceof TKeypressEvent) {
                 dispatchToDesktop = false;
+            } else if (event instanceof TMenuEvent) {
+                dispatchToDesktop = false;
             }
 
             if (debugEvents) {
@@ -1161,6 +1461,8 @@ public class TApplication implements Runnable {
 
         // Peek at the mouse position
         if (event instanceof TMouseEvent) {
+            typingHidMouse = false;
+
             TMouseEvent mouse = (TMouseEvent) event;
             if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
                 oldMouseX = mouseX;
@@ -1182,7 +1484,8 @@ public class TApplication implements Runnable {
                             mouse.getAbsoluteX(), mouse.getAbsoluteY(),
                             mouse.isMouse1(), mouse.isMouse2(),
                             mouse.isMouse3(),
-                            mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+                            mouse.isMouseWheelUp(), mouse.isMouseWheelDown(),
+                            mouse.isAlt(), mouse.isCtrl(), mouse.isShift());
 
                     } else {
                         // The first click of a potential double-click.
@@ -1320,6 +1623,19 @@ public class TApplication implements Runnable {
         }
     }
 
+    /**
+     * Wake the sleeping screen handler.
+     */
+    private void wakeScreenHandler() {
+        if (!started) {
+            return;
+        }
+
+        synchronized (screenHandler) {
+            screenHandler.notify();
+        }
+    }
+
     // ------------------------------------------------------------------------
     // TApplication -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -1385,6 +1701,15 @@ public class TApplication implements Runnable {
         return theme;
     }
 
+    /**
+     * Get the clipboard.
+     *
+     * @return the clipboard
+     */
+    public final Clipboard getClipboard() {
+        return clipboard;
+    }
+
     /**
      * Repaint the screen on the next update.
      */
@@ -1419,6 +1744,8 @@ public class TApplication implements Runnable {
      */
     public final void setDesktop(final TDesktop desktop) {
         if (this.desktop != null) {
+            this.desktop.onPreClose();
+            this.desktop.onUnfocus();
             this.desktop.onClose();
         }
         this.desktop = desktop;
@@ -1480,7 +1807,7 @@ public class TApplication implements Runnable {
         String version = getClass().getPackage().getImplementationVersion();
         if (version == null) {
             // This is Java 9+, use a hardcoded string here.
-            version = "0.3.0";
+            version = "1.0.0";
         }
         messageBox(i18n.getString("aboutDialogTitle"),
             MessageFormat.format(i18n.getString("aboutDialogText"), version),
@@ -1525,14 +1852,15 @@ public class TApplication implements Runnable {
     // ------------------------------------------------------------------------
 
     /**
-     * Invert the cell color at a position.  This is used to track the mouse.
+     * Draw the text mouse at position.
      *
      * @param x column position
      * @param y row position
      */
-    private void invertCell(final int x, final int y) {
+    private void drawTextMouse(final int x, final int y) {
+
         if (debugThreads) {
-            System.err.printf("%d %s invertCell() %d %d\n",
+            System.err.printf("%d %s drawTextMouse() %d %d\n",
                 System.currentTimeMillis(), Thread.currentThread(), x, y);
 
             if (activeWindow != null) {
@@ -1554,22 +1882,20 @@ public class TApplication implements Runnable {
             }
         }
 
-        Cell cell = getScreen().getCharXY(x, y);
-        if (cell.isImage()) {
-            cell.invertImage();
-        } else {
-            if (cell.getForeColorRGB() < 0) {
-                cell.setForeColor(cell.getForeColor().invert());
-            } else {
-                cell.setForeColorRGB(cell.getForeColorRGB() ^ 0x00ffffff);
-            }
-            if (cell.getBackColorRGB() < 0) {
-                cell.setBackColor(cell.getBackColor().invert());
-            } else {
-                cell.setBackColorRGB(cell.getBackColorRGB() ^ 0x00ffffff);
+        // If this cell is on top of the desktop, and the desktop has
+        // requested a hidden mouse, bail out.
+        if ((desktop != null) && (activeWindow == null) && (activeMenu == null)) {
+            if ((desktop.hasHiddenMouse() == true)
+                && (x > desktop.getX())
+                && (x < desktop.getX() + desktop.getWidth() - 1)
+                && (y > desktop.getY())
+                && (y < desktop.getY() + desktop.getHeight() - 1)
+            ) {
+                return;
             }
         }
-        getScreen().putCharXY(x, y, cell);
+
+        getScreen().invertCell(x, y);
     }
 
     /**
@@ -1608,7 +1934,7 @@ public class TApplication implements Runnable {
                 getScreen().putCharXY(oldDrawnMouseX, oldDrawnMouseY,
                     oldDrawnMouseCell);
                 oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY);
-                if ((images.size() > 0) && (backend instanceof ECMA48Backend)) {
+                if (backend instanceof ECMA48Backend) {
                     // Special case: the entire row containing the mouse has
                     // to be re-drawn if it has any image data, AND any rows
                     // in between.
@@ -1629,14 +1955,22 @@ public class TApplication implements Runnable {
                     }
                 }
 
-                // Draw mouse at the new position.
-                invertCell(mouseX, mouseY);
+                if (inScreenSelection) {
+                    getScreen().setSelection(screenSelectionX0,
+                        screenSelectionY0, screenSelectionX1, screenSelectionY1,
+                        screenSelectionRectangle);
+                }
+
+                if ((textMouse == true) && (typingHidMouse == false)) {
+                    // Draw mouse at the new position.
+                    drawTextMouse(mouseX, mouseY);
+                }
 
                 oldDrawnMouseX = mouseX;
                 oldDrawnMouseY = mouseY;
             }
-            if ((images.size() > 0) || getScreen().isDirty()) {
-                backend.flushScreen();
+            if (getScreen().isDirty()) {
+                screenHandler.setDirty();
             }
             return;
         }
@@ -1671,63 +2005,71 @@ public class TApplication implements Runnable {
             }
         }
 
-        // 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()) {
-                ((TWindow) menu).drawChildren();
-                // Reset the screen clipping so we can draw the next title.
-                getScreen().resetClipping();
-            }
-            x += menu.getTitle().length() + 2;
-        }
+        if (hideMenuBar == false) {
 
-        for (TMenu menu: subMenus) {
-            // Reset the screen clipping so we can draw the next sub-menu.
+            // Draw the blank menubar line - reset the screen clipping first
+            // so it won't trim it out.
             getScreen().resetClipping();
-            ((TWindow) menu).drawChildren();
+            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,
+                    StringUtils.width(menu.getTitle()) + 2, ' ', menuColor);
+                getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor);
+                // Draw the highlight character
+                getScreen().putCharXY(x + 1 +
+                    menu.getMnemonic().getScreenShortcutIdx(),
+                    0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
+
+                if (menu.isActive()) {
+                    ((TWindow) menu).drawChildren();
+                    // Reset the screen clipping so we can draw the next
+                    // title.
+                    getScreen().resetClipping();
+                }
+                x += StringUtils.width(menu.getTitle()) + 2;
+            }
+
+            for (TMenu menu: subMenus) {
+                // Reset the screen clipping so we can draw the next
+                // sub-menu.
+                getScreen().resetClipping();
+                ((TWindow) menu).drawChildren();
+            }
         }
         getScreen().resetClipping();
 
-        // 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 (hideStatusBar == false) {
+            // 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);
+            }
         }
 
         // Draw the mouse pointer
@@ -1737,7 +2079,7 @@ public class TApplication implements Runnable {
                 oldDrawnMouseX, oldDrawnMouseY);
         }
         oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY);
-        if ((images.size() > 0) && (backend instanceof ECMA48Backend)) {
+        if (backend instanceof ECMA48Backend) {
             // Special case: the entire row containing the mouse has to be
             // re-drawn if it has any image data, AND any rows in between.
             if (oldDrawnMouseY != mouseY) {
@@ -1756,18 +2098,43 @@ public class TApplication implements Runnable {
                 getScreen().unsetImageRow(mouseY);
             }
         }
-        invertCell(mouseX, mouseY);
+
+        if (inScreenSelection) {
+            getScreen().setSelection(screenSelectionX0, screenSelectionY0,
+                screenSelectionX1, screenSelectionY1, screenSelectionRectangle);
+        }
+
+        if ((textMouse == true) && (typingHidMouse == false)) {
+            drawTextMouse(mouseX, mouseY);
+        }
         oldDrawnMouseX = mouseX;
         oldDrawnMouseY = mouseY;
 
         // Place the cursor if it is visible
         if (!menuIsActive) {
+
+            int visibleWindowCount = 0;
+            for (TWindow window: sorted) {
+                if (window.isShown()) {
+                    visibleWindowCount++;
+                }
+            }
+            if (visibleWindowCount == 0) {
+                // No windows are visible, only the desktop.  Allow it to
+                // have the cursor.
+                if (desktop != null) {
+                    sorted.add(desktop);
+                }
+            }
+
             TWidget activeWidget = null;
             if (sorted.size() > 0) {
                 activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
+                int cursorClipTop = desktopTop;
+                int cursorClipBottom = desktopBottom;
                 if (activeWidget.isCursorVisible()) {
-                    if ((activeWidget.getCursorAbsoluteY() < desktopBottom)
-                        && (activeWidget.getCursorAbsoluteY() > desktopTop)
+                    if ((activeWidget.getCursorAbsoluteY() <= cursorClipBottom)
+                        && (activeWidget.getCursorAbsoluteY() >= cursorClipTop)
                     ) {
                         getScreen().putCursor(true,
                             activeWidget.getCursorAbsoluteX(),
@@ -1787,15 +2154,9 @@ public class TApplication implements Runnable {
             getScreen().hideCursor();
         }
 
-        // Flush the screen contents
-        if ((images.size() > 0) || getScreen().isDirty()) {
-            if (debugThreads) {
-                System.err.printf("%d %s backend.flushScreen()\n",
-                    System.currentTimeMillis(), Thread.currentThread());
-            }
-            backend.flushScreen();
+        if (getScreen().isDirty()) {
+            screenHandler.setDirty();
         }
-
         repaint = false;
     }
 
@@ -1809,6 +2170,14 @@ public class TApplication implements Runnable {
         }
     }
 
+    /**
+     * Subclasses can use this hook to cleanup resources.  Called as the last
+     * step of TApplication.run().
+     */
+    public void onExit() {
+        // Default does nothing.
+    }
+
     // ------------------------------------------------------------------------
     // TWindow management -----------------------------------------------------
     // ------------------------------------------------------------------------
@@ -1924,9 +2293,6 @@ public class TApplication implements Runnable {
 
         assert (!window.isActive());
         if (activeWindow != null) {
-            // TODO: see if this assertion is really necessary.
-            // assert (activeWindow.getZ() == 0);
-
             activeWindow.setActive(false);
 
             // Increment every window Z that is on top of window
@@ -2227,6 +2593,7 @@ public class TApplication implements Runnable {
             if (((window.flags & TWindow.CENTERED) == 0)
                 && ((window.flags & TWindow.ABSOLUTEXY) == 0)
                 && (smartWindowPlacement == true)
+                && (!(window instanceof TDesktop))
             ) {
 
                 doSmartPlacement(window);
@@ -2512,46 +2879,6 @@ public class TApplication implements Runnable {
         window.setY(windowY);
     }
 
-    // ------------------------------------------------------------------------
-    // TImage management ------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Add an image to the list.  Note package private access.
-     *
-     * @param image the image to add
-     * @throws IllegalArgumentException if the image is already used in
-     * another TApplication
-     */
-    final void addImage(final TImage image) {
-        if ((image.getApplication() != null)
-            && (image.getApplication() != this)
-        ) {
-            throw new IllegalArgumentException("Image " + image +
-                " is already " + "part of application " +
-                image.getApplication());
-        }
-        images.add(image);
-    }
-
-    /**
-     * Remove an image from the list.  Note package private access.
-     *
-     * @param image the image to remove
-     * @throws IllegalArgumentException if the image is already used in
-     * another TApplication
-     */
-    final void removeImage(final TImage image) {
-        if ((image.getApplication() != null)
-            && (image.getApplication() != this)
-        ) {
-            throw new IllegalArgumentException("Image " + image +
-                " is already " + "part of application " +
-                image.getApplication());
-        }
-        images.remove(image);
-    }
-
     // ------------------------------------------------------------------------
     // TMenu management -------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -2605,6 +2932,7 @@ public class TApplication implements Runnable {
             && (!modalWindowActive())
             && (!overrideMenuWindowActive())
             && (mouse.getAbsoluteY() == 0)
+            && (hideMenuBar == false)
         ) {
 
             for (TMenu menu: subMenus) {
@@ -2616,7 +2944,7 @@ public class TApplication implements Runnable {
             for (TMenu menu: menus) {
                 if ((mouse.getAbsoluteX() >= menu.getTitleX())
                     && (mouse.getAbsoluteX() < menu.getTitleX()
-                        + menu.getTitle().length() + 2)
+                        + StringUtils.width(menu.getTitle()) + 2)
                 ) {
                     menu.setActive(true);
                     activeMenu = menu;
@@ -2632,6 +2960,7 @@ public class TApplication implements Runnable {
             && (mouse.isMouse1())
             && (activeMenu != null)
             && (mouse.getAbsoluteY() == 0)
+            && (hideMenuBar == false)
         ) {
 
             TMenu oldMenu = activeMenu;
@@ -2644,7 +2973,7 @@ public class TApplication implements Runnable {
             for (TMenu menu: menus) {
                 if ((mouse.getAbsoluteX() >= menu.getTitleX())
                     && (mouse.getAbsoluteX() < menu.getTitleX()
-                        + menu.getTitle().length() + 2)
+                        + StringUtils.width(menu.getTitle()) + 2)
                 ) {
                     menu.setActive(true);
                     activeMenu = menu;
@@ -2800,6 +3129,7 @@ public class TApplication implements Runnable {
      */
     public final void switchMenu(final boolean forward) {
         assert (activeMenu != null);
+        assert (hideMenuBar == false);
 
         for (TMenu menu: subMenus) {
             menu.setActive(false);
@@ -2906,6 +3236,21 @@ public class TApplication implements Runnable {
         }
     }
 
+    /**
+     * Get the menu item associated with this ID.
+     *
+     * @param id the menu item ID
+     * @return the menu item, or null if not found
+     */
+    public final TMenuItem getMenuItem(final int id) {
+        for (TMenuItem item: menuItems) {
+            if (item.getId() == id) {
+                return item;
+            }
+        }
+        return null;
+    }
+
     /**
      * Recompute menu x positions based on their title length.
      */
@@ -2914,7 +3259,7 @@ public class TApplication implements Runnable {
         for (TMenu menu: menus) {
             menu.setX(x);
             menu.setTitleX(x);
-            x += menu.getTitle().length() + 2;
+            x += StringUtils.width(menu.getTitle()) + 2;
 
             // Don't let the menu window exceed the screen width
             int rightEdge = menu.getX() + menu.getWidth();
@@ -2994,7 +3339,7 @@ public class TApplication implements Runnable {
         TMenu toolMenu = addMenu(i18n.getString("toolMenuTitle"));
         toolMenu.addDefaultItem(TMenu.MID_REPAINT);
         toolMenu.addDefaultItem(TMenu.MID_VIEW_IMAGE);
-        toolMenu.addDefaultItem(TMenu.MID_CHANGE_FONT);
+        toolMenu.addDefaultItem(TMenu.MID_SCREEN_OPTIONS);
         TStatusBar toolStatusBar = toolMenu.newStatusBar(i18n.
             getString("toolMenuStatus"));
         toolStatusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
@@ -3008,9 +3353,8 @@ public class TApplication implements Runnable {
      */
     public final TMenu addFileMenu() {
         TMenu fileMenu = addMenu(i18n.getString("fileMenuTitle"));
-        fileMenu.addDefaultItem(TMenu.MID_OPEN_FILE);
-        fileMenu.addSeparator();
         fileMenu.addDefaultItem(TMenu.MID_SHELL);
+        fileMenu.addSeparator();
         fileMenu.addDefaultItem(TMenu.MID_EXIT);
         TStatusBar statusBar = fileMenu.newStatusBar(i18n.
             getString("fileMenuStatus"));
@@ -3025,10 +3369,10 @@ public class TApplication implements Runnable {
      */
     public final TMenu addEditMenu() {
         TMenu editMenu = addMenu(i18n.getString("editMenuTitle"));
-        editMenu.addDefaultItem(TMenu.MID_CUT);
-        editMenu.addDefaultItem(TMenu.MID_COPY);
-        editMenu.addDefaultItem(TMenu.MID_PASTE);
-        editMenu.addDefaultItem(TMenu.MID_CLEAR);
+        editMenu.addDefaultItem(TMenu.MID_CUT, false);
+        editMenu.addDefaultItem(TMenu.MID_COPY, false);
+        editMenu.addDefaultItem(TMenu.MID_PASTE, false);
+        editMenu.addDefaultItem(TMenu.MID_CLEAR, false);
         TStatusBar statusBar = editMenu.newStatusBar(i18n.
             getString("editMenuStatus"));
         statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
@@ -3078,6 +3422,64 @@ public class TApplication implements Runnable {
         return helpMenu;
     }
 
+    /**
+     * Convenience function to add a default "Table" menu.
+     *
+     * @return the new menu
+     */
+    public final TMenu addTableMenu() {
+        TMenu tableMenu = addMenu(i18n.getString("tableMenuTitle"));
+        tableMenu.addDefaultItem(TMenu.MID_TABLE_RENAME_COLUMN, false);
+        tableMenu.addDefaultItem(TMenu.MID_TABLE_RENAME_ROW, false);
+        tableMenu.addSeparator();
+
+        TSubMenu viewMenu = tableMenu.addSubMenu(i18n.
+            getString("tableSubMenuView"));
+        viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_ROW_LABELS, false);
+        viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_COLUMN_LABELS, false);
+        viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW, false);
+        viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN, false);
+
+        TSubMenu borderMenu = tableMenu.addSubMenu(i18n.
+            getString("tableSubMenuBorders"));
+        borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_NONE, false);
+        borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_ALL, false);
+        borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_CELL_NONE, false);
+        borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_CELL_ALL, false);
+        borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_RIGHT, false);
+        borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_LEFT, false);
+        borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_TOP, false);
+        borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_BOTTOM, false);
+        borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_DOUBLE_BOTTOM, false);
+        borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_THICK_BOTTOM, false);
+        TSubMenu deleteMenu = tableMenu.addSubMenu(i18n.
+            getString("tableSubMenuDelete"));
+        deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_LEFT, false);
+        deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_UP, false);
+        deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_ROW, false);
+        deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_COLUMN, false);
+        TSubMenu insertMenu = tableMenu.addSubMenu(i18n.
+            getString("tableSubMenuInsert"));
+        insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_LEFT, false);
+        insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_RIGHT, false);
+        insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_ABOVE, false);
+        insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_BELOW, false);
+        TSubMenu columnMenu = tableMenu.addSubMenu(i18n.
+            getString("tableSubMenuColumn"));
+        columnMenu.addDefaultItem(TMenu.MID_TABLE_COLUMN_NARROW, false);
+        columnMenu.addDefaultItem(TMenu.MID_TABLE_COLUMN_WIDEN, false);
+        TSubMenu fileMenu = tableMenu.addSubMenu(i18n.
+            getString("tableSubMenuFile"));
+        fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_OPEN_CSV, false);
+        fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_SAVE_CSV, false);
+        fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_SAVE_TEXT, false);
+
+        TStatusBar statusBar = tableMenu.newStatusBar(i18n.
+            getString("tableMenuStatus"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
+        return tableMenu;
+    }
+
     // ------------------------------------------------------------------------
     // TTimer management ------------------------------------------------------
     // ------------------------------------------------------------------------