Merge branch 'subtree'
[fanfix.git] / src / jexer / TWindow.java
index 6cd11e5982fface1b1b8a44a29000c6cb8ddb794..4d14d0eee2debcf23b03e2df314ea41721c38c8c 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
 package jexer;
 
 import java.util.HashSet;
+import java.util.Set;
 
-import jexer.bits.Cell;
+import jexer.backend.Screen;
 import jexer.bits.CellAttributes;
 import jexer.bits.GraphicsChars;
+import jexer.bits.StringUtils;
 import jexer.event.TCommandEvent;
 import jexer.event.TKeypressEvent;
 import jexer.event.TMenuEvent;
 import jexer.event.TMouseEvent;
 import jexer.event.TResizeEvent;
-import jexer.io.Screen;
 import jexer.menu.TMenu;
 import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
@@ -49,7 +50,7 @@ import static jexer.TKeypress.*;
 public class TWindow extends TWidget {
 
     // ------------------------------------------------------------------------
-    // Public constants -------------------------------------------------------
+    // Constants --------------------------------------------------------------
     // ------------------------------------------------------------------------
 
     /**
@@ -73,162 +74,58 @@ public class TWindow extends TWidget {
      */
     public static final int NOCLOSEBOX  = 0x08;
 
-    // ------------------------------------------------------------------------
-    // Common window attributes -----------------------------------------------
-    // ------------------------------------------------------------------------
-
     /**
-     * Window flags.  Note package private access.
+     * Window has no maximize box (default no).
      */
-    int flags = RESIZABLE;
+    public static final int NOZOOMBOX   = 0x10;
 
     /**
-     * Window title.
+     * Window is placed at absolute position (no smart placement) (default
+     * no).
      */
-    private String title = "";
+    public static final int ABSOLUTEXY  = 0x20;
 
     /**
-     * Get window title.
-     *
-     * @return window title
+     * Hitting the closebox with the mouse calls TApplication.hideWindow()
+     * rather than TApplication.closeWindow() (default no).
      */
-    public final String getTitle() {
-        return title;
-    }
+    public static final int HIDEONCLOSE = 0x40;
 
     /**
-     * Set window title.
-     *
-     * @param title new window title
+     * Menus cannot be used when this window is active (default no).
      */
-    public final void setTitle(final String title) {
-        this.title = title;
-    }
+    public static final int OVERRIDEMENU        = 0x80;
 
     // ------------------------------------------------------------------------
-    // 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
@@ -246,7 +143,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.
@@ -278,72 +175,6 @@ public class TWindow extends TWidget {
     private int restoreWindowX;
     private int restoreWindowY;
 
-    /**
-     * Set the maximum width for this window.
-     *
-     * @param maximumWindowWidth new maximum width
-     */
-    public final void setMaximumWindowWidth(final int maximumWindowWidth) {
-        this.maximumWindowWidth = maximumWindowWidth;
-    }
-
-    /**
-     * 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;
-    }
-
-    /**
-     * 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
@@ -353,53 +184,25 @@ public class TWindow extends TWidget {
     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.
+     * 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 void show() {
-        application.showWindow(this);
-    }
+    protected TStatusBar statusBar = null;
 
     /**
-     * Activate window (bring to top and receive events).
+     * A window may request that TApplication NOT draw the mouse cursor over
+     * it by setting this to true.  This is currently only used within Jexer
+     * by TTerminalWindow so that only the bottom-most instance of nested
+     * Jexer's draws the mouse within its application window.  But perhaps
+     * other applications can use it, so public getter/setter is provided.
      */
-    public void activate() {
-        application.activateWindow(this);
-    }
+    private boolean hideMouse = false;
 
     /**
-     * Close window.  Note that windows without a close box can still be
-     * closed by calling the close() method.
+     * The help topic for this window.
      */
-    public void close() {
-        application.closeWindow(this);
-    }
+    protected String helpTopic = "Help";
 
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
@@ -489,218 +292,40 @@ public class TWindow extends TWidget {
         center();
 
         // Add me to the application
-        application.addWindow(this);
+        application.addWindowToApplication(this);
     }
 
     // ------------------------------------------------------------------------
-    // General behavior -------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
     // ------------------------------------------------------------------------
 
     /**
-     * Returns true if this window is modal.
+     * Returns true if the mouse is currently on the close button.
      *
-     * @return true if this window is modal
+     * @return true if mouse is currently on the close button
      */
-    public final boolean isModal() {
-        if ((flags & MODAL) == 0) {
+    protected boolean mouseOnClose() {
+        if ((flags & NOCLOSEBOX) != 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) {
+        if ((mouse != null)
+            && (mouse.getAbsoluteY() == getY())
+            && (mouse.getAbsoluteX() == getX() + 3)
+        ) {
             return true;
         }
         return false;
     }
 
     /**
-     * Retrieve the background color.
-     *
-     * @return the background color
-     */
-    public final 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 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()) {
-
-                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.
+     * 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())
@@ -731,19 +356,32 @@ public class TWindow extends TWidget {
         return false;
     }
 
+    /**
+     * Subclasses should override this method to perform any user prompting
+     * before they are offscreen.  Note that unlike other windowing toolkits,
+     * windows can NOT use this function in some manner to avoid being
+     * closed.  This is called by application.closeWindow().
+     */
+    protected void onPreClose() {
+        // Default: do nothing.
+    }
+
     /**
      * Subclasses should override this method to cleanup resources.  This is
      * called by application.closeWindow().
      */
-    public void onClose() {
-        // Default: do nothing
+    protected void onClose() {
+        // Default: perform widget-specific cleanup.
+        for (TWidget w: getChildren()) {
+            w.close();
+        }
     }
 
     /**
      * Called by application.switchWindow() when this window gets the
      * focus, and also by application.addWindow().
      */
-    public void onFocus() {
+    protected void onFocus() {
         // Default: do nothing
     }
 
@@ -751,21 +389,21 @@ public class TWindow extends TWidget {
      * Called by application.switchWindow() when another window gets the
      * focus.
      */
-    public void onUnfocus() {
+    protected void onUnfocus() {
         // Default: do nothing
     }
 
     /**
      * Called by application.hideWindow().
      */
-    public void onHide() {
+    protected void onHide() {
         // Default: do nothing
     }
 
     /**
      * Called by application.showWindow().
      */
-    public void onShow() {
+    protected void onShow() {
         // Default: do nothing
     }
 
@@ -779,6 +417,8 @@ public class TWindow extends TWidget {
         this.mouse = mouse;
 
         inKeyboardResize = false;
+        inWindowMove = false;
+        inWindowResize = false;
 
         if ((mouse.getAbsoluteY() == getY())
             && mouse.isMouse1()
@@ -844,8 +484,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;
         }
 
@@ -920,23 +565,22 @@ public class TWindow extends TWidget {
             // Keep within min/max bounds
             if (getWidth() < minimumWindowWidth) {
                 setWidth(minimumWindowWidth);
-                inWindowResize = false;
             }
             if (getHeight() < minimumWindowHeight) {
                 setHeight(minimumWindowHeight);
-                inWindowResize = false;
             }
             if ((maximumWindowWidth > 0)
                 && (getWidth() > maximumWindowWidth)
             ) {
                 setWidth(maximumWindowWidth);
-                inWindowResize = false;
             }
             if ((maximumWindowHeight > 0)
                 && (getHeight() > maximumWindowHeight)
             ) {
                 setHeight(maximumWindowHeight);
-                inWindowResize = false;
+            }
+            if (getHeight() + getY() >= getApplication().getDesktopBottom()) {
+                setHeight(getApplication().getDesktopBottom() - getY());
             }
 
             // Pass a resize event to my children
@@ -962,6 +606,15 @@ public class TWindow extends TWidget {
     @Override
     public void onKeypress(final TKeypressEvent keypress) {
 
+        if (inWindowMove || inWindowResize) {
+            // ESC or ENTER - Exit size/move
+            if (keypress.equals(kbEsc) || keypress.equals(kbEnter)) {
+                inWindowMove = false;
+                inWindowResize = false;
+                return;
+            }
+        }
+
         if (inKeyboardResize) {
 
             // ESC or ENTER - Exit size/move
@@ -989,38 +642,46 @@ public class TWindow extends TWidget {
                     setY(getY() - 1);
                 }
             }
-            if (keypress.equals(kbShiftLeft)) {
-                if ((getWidth() > minimumWindowWidth)
-                    || (minimumWindowWidth <= 0)
-                ) {
-                    setWidth(getWidth() - 1);
+
+            /*
+             * Only permit keyboard resizing if the window was RESIZABLE.
+             */
+            if ((flags & RESIZABLE) != 0) {
+
+                if (keypress.equals(kbShiftLeft)) {
+                    if ((getWidth() > minimumWindowWidth)
+                        || (minimumWindowWidth <= 0)
+                    ) {
+                        setWidth(getWidth() - 1);
+                    }
                 }
-            }
-            if (keypress.equals(kbShiftRight)) {
-                if ((getWidth() < maximumWindowWidth)
-                    || (maximumWindowWidth <= 0)
-                ) {
-                    setWidth(getWidth() + 1);
+                if (keypress.equals(kbShiftRight)) {
+                    if ((getWidth() < maximumWindowWidth)
+                        || (maximumWindowWidth <= 0)
+                    ) {
+                        setWidth(getWidth() + 1);
+                    }
                 }
-            }
-            if (keypress.equals(kbShiftUp)) {
-                if ((getHeight() > minimumWindowHeight)
-                    || (minimumWindowHeight <= 0)
-                ) {
-                    setHeight(getHeight() - 1);
+                if (keypress.equals(kbShiftUp)) {
+                    if ((getHeight() > minimumWindowHeight)
+                        || (minimumWindowHeight <= 0)
+                    ) {
+                        setHeight(getHeight() - 1);
+                    }
                 }
-            }
-            if (keypress.equals(kbShiftDown)) {
-                if ((getHeight() < maximumWindowHeight)
-                    || (maximumWindowHeight <= 0)
-                ) {
-                    setHeight(getHeight() + 1);
+                if (keypress.equals(kbShiftDown)) {
+                    if ((getHeight() < maximumWindowHeight)
+                        || (maximumWindowHeight <= 0)
+                    ) {
+                        setHeight(getHeight() + 1);
+                    }
                 }
-            }
 
-            // Pass a resize event to my children
-            onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
-                    getWidth(), getHeight()));
+                // Pass a resize event to my children
+                onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+                        getWidth(), getHeight()));
+
+            } // if ((flags & RESIZABLE) != 0)
 
             return;
         }
@@ -1041,7 +702,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;
             }
@@ -1059,7 +726,7 @@ public class TWindow extends TWidget {
             }
 
             // F5 - zoom
-            if (keypress.equals(kbF5)) {
+            if (keypress.equals(kbF5) && ((flags & NOZOOMBOX) == 0)) {
                 if (maximized) {
                     restore();
                 } else {
@@ -1094,7 +761,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;
             }
@@ -1114,7 +787,7 @@ public class TWindow extends TWidget {
                 return;
             }
 
-            if (command.equals(cmWindowZoom)) {
+            if (command.equals(cmWindowZoom) && ((flags & NOZOOMBOX) == 0)) {
                 if (maximized) {
                     restore();
                 } else {
@@ -1140,7 +813,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;
             }
@@ -1160,7 +839,9 @@ public class TWindow extends TWidget {
                 return;
             }
 
-            if (menu.getId() == TMenu.MID_WINDOW_ZOOM) {
+            if ((menu.getId() == TMenu.MID_WINDOW_ZOOM)
+                && ((flags & NOZOOMBOX) == 0)
+            ) {
                 if (maximized) {
                     restore();
                 } else {
@@ -1175,149 +856,608 @@ public class TWindow extends TWidget {
         super.onMenu(menu);
     }
 
-    // ------------------------------------------------------------------------
-    // Passthru for Screen functions ------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Get the attributes at one location.
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @return attributes at (x, y)
-     */
-    public final CellAttributes getAttrXY(final int x, final int y) {
-        return getScreen().getAttrXY(x, y);
-    }
-
     /**
-     * Set the attributes at one location.
+     * Method that subclasses can override to handle window/screen resize
+     * events.
      *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param attr attributes to use (bold, foreColor, backColor)
+     * @param resize resize event
      */
-    public final void putAttrXY(final int x, final int y,
-        final CellAttributes attr) {
+    @Override
+    public void onResize(final TResizeEvent resize) {
+        if (resize.getType() == TResizeEvent.Type.WIDGET) {
+            if (getChildren().size() == 1) {
+                TWidget child = getChildren().get(0);
+                if ((child instanceof TSplitPane)
+                    || (child instanceof TPanel)
+                ) {
+                    if (this instanceof TDesktop) {
+                        child.onResize(new TResizeEvent(
+                            TResizeEvent.Type.WIDGET,
+                            resize.getWidth(), resize.getHeight()));
+                    } else {
+                        child.onResize(new TResizeEvent(
+                            TResizeEvent.Type.WIDGET,
+                            resize.getWidth() - 2, resize.getHeight() - 2));
+                    }
+                }
+                return;
+            }
+        }
 
-        getScreen().putAttrXY(x, y, attr);
+        // Pass on to TWidget.
+        super.onResize(resize);
     }
 
-    /**
-     * Set the attributes at one location.
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param attr attributes to use (bold, foreColor, backColor)
-     * @param clip if true, honor clipping/offset
-     */
-    public final void putAttrXY(final int x, final int y,
-        final CellAttributes attr, final boolean clip) {
-
-        getScreen().putAttrXY(x, y, attr, clip);
-    }
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
 
     /**
-     * Fill the entire screen with one character with attributes.
+     * Get this TWindow's parent TApplication.
      *
-     * @param ch character to draw
-     * @param attr attributes to use (bold, foreColor, backColor)
+     * @return this TWindow's parent TApplication
      */
-    public final void putAll(final char ch, final CellAttributes attr) {
-        getScreen().putAll(ch, attr);
+    @Override
+    public final TApplication getApplication() {
+        return application;
     }
 
     /**
-     * Render one character with attributes.
+     * Get the Screen.
      *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param ch character + attributes to draw
+     * @return the Screen
      */
-    public final void putCharXY(final int x, final int y, final Cell ch) {
-        getScreen().putCharXY(x, y, ch);
+    @Override
+    public final Screen getScreen() {
+        return application.getScreen();
     }
 
     /**
-     * Render one character with attributes.
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param ch character to draw
-     * @param attr attributes to use (bold, foreColor, backColor)
+     * Called by TApplication.drawChildren() to render on screen.
      */
-    public final void putCharXY(final int x, final int y, final char ch,
-        final CellAttributes attr) {
-
-        getScreen().putCharXY(x, y, ch, attr);
-    }
+    @Override
+    public void draw() {
+        // Draw the box and background first.
+        CellAttributes border = getBorder();
+        CellAttributes background = getBackground();
+        int borderType = getBorderType();
 
-    /**
-     * Render one character without changing the underlying attributes.
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param ch character to draw
-     */
-    public final void putCharXY(final int x, final int y, final char ch) {
-        getScreen().putCharXY(x, y, ch);
-    }
+        drawBox(0, 0, getWidth(), getHeight(), border, background, borderType,
+            true);
 
-    /**
-     * Render a string.  Does not wrap if the string exceeds the line.
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param str string to draw
-     * @param attr attributes to use (bold, foreColor, backColor)
-     */
-    public final void putStringXY(final int x, final int y, final String str,
-        final CellAttributes attr) {
+        // Draw the title
+        int titleLength = StringUtils.width(title);
+        int titleLeft = (getWidth() - titleLength - 2) / 2;
+        putCharXY(titleLeft, 0, ' ', border);
+        putStringXY(titleLeft + 1, 0, title, border);
+        putCharXY(titleLeft + titleLength + 1, 0, ' ', border);
 
-        getScreen().putStringXY(x, y, str, attr);
-    }
+        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() - application.getDesktopTop());
+        setX(0);
+        setY(application.getDesktopTop());
+        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).
+     */
+    @Override
+    public void activate() {
+        application.activateWindow(this);
+    }
+
+    /**
+     * Close window.  Note that windows without a close box can still be
+     * closed by calling the close() method.
+     */
+    @Override
+    public void close() {
+        application.closeWindow(this);
+    }
 
     /**
-     * Render a string without changing the underlying attribute.  Does not
-     * wrap if the string exceeds the line.
+     * See if this window is undergoing any movement/resize/etc.
      *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param str string to draw
+     * @return true if the window is moving
      */
-    public final void putStringXY(final int x, final int y, final String str) {
-        getScreen().putStringXY(x, y, str);
+    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;
     }
 
     /**
-     * Draw a vertical line from (x, y) to (x, y + n).
+     * Returns true if this window is modal.
      *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param n number of characters to draw
-     * @param ch character to draw
-     * @param attr attributes to use (bold, foreColor, backColor)
+     * @return true if this window is modal
      */
-    public final void vLineXY(final int x, final int y, final int n,
-        final char ch, final CellAttributes attr) {
+    public final boolean isModal() {
+        if ((flags & MODAL) == 0) {
+            return false;
+        }
+        return true;
+    }
 
-        getScreen().vLineXY(x, y, n, ch, attr);
+    /**
+     * 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;
     }
 
     /**
-     * Draw a horizontal line from (x, y) to (x + n, y).
+     * Returns true if this window has a maximize/zoom box.
      *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param n number of characters to draw
-     * @param ch character to draw
-     * @param attr attributes to use (bold, foreColor, backColor)
+     * @return true if this window has a maximize/zoom box
      */
-    public final void hLineXY(final int x, final int y, final int n,
-        final char ch, final CellAttributes attr) {
+    public final boolean hasZoomBox() {
+        if ((flags & NOZOOMBOX) != 0) {
+            return true;
+        }
+        return false;
+    }
 
-        getScreen().hLineXY(x, y, n, ch, attr);
+    /**
+     * Returns true if this window does not want menus to work while it is
+     * visible.
+     *
+     * @return true if this window does not want menus to work while it is
+     * visible
+     */
+    public final boolean hasOverriddenMenu() {
+        if ((flags & OVERRIDEMENU) != 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)
+        ) {
+            if (!isActive()) {
+                // The user's terminal never passed a mouse up event, and now
+                // another window is active but we never finished a drag.
+                inWindowMove = false;
+                inWindowResize = false;
+                inKeyboardResize = false;
+                return getTheme().getColor("twindow.border.inactive");
+            }
+
+            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;
+        }
+    }
+
+    /**
+     * Returns true if this window does not want the application-wide mouse
+     * cursor drawn over it.
+     *
+     * @return true if this window does not want the application-wide mouse
+     * cursor drawn over it
+     */
+    public boolean hasHiddenMouse() {
+        return hideMouse;
+    }
+
+    /**
+     * Set request to prevent the application-wide mouse cursor from being
+     * drawn over this window.
+     *
+     * @param hideMouse if true, this window does not want the
+     * application-wide mouse cursor drawn over it
+     */
+    public final void setHiddenMouse(final boolean hideMouse) {
+        this.hideMouse = hideMouse;
+    }
+
+    /**
+     * Get this window's help topic to load.
+     *
+     * @return the topic name
+     */
+    public String getHelpTopic() {
+        return helpTopic;
+    }
+
+    /**
+     * Generate a human-readable string for this window.
+     *
+     * @return a human-readable string
+     */
+    @Override
+    public String toString() {
+        return String.format("%s(%8x) \'%s\' Z %d position (%d, %d) " +
+            "geometry %dx%d  hidden %s modal %s",
+            getClass().getName(), hashCode(), title, getZ(),
+            getX(), getY(), getWidth(), getHeight(), hidden, isModal());
+    }
 
 }