TWindow compiles
authorKevin Lamonte <kevin.lamonte@gmail.com>
Thu, 12 Mar 2015 18:14:00 +0000 (14:14 -0400)
committerKevin Lamonte <kevin.lamonte@gmail.com>
Thu, 12 Mar 2015 18:14:00 +0000 (14:14 -0400)
Makefile
src/jexer/TApplication.java
src/jexer/THScroller.java [new file with mode: 0644]
src/jexer/TMenu.java [new file with mode: 0644]
src/jexer/TVScroller.java [new file with mode: 0644]
src/jexer/TWidget.java [new file with mode: 0644]
src/jexer/TWindow.java [new file with mode: 0644]
src/jexer/bits/GraphicsChars.java
src/jexer/bits/MnemonicString.java
src/jexer/event/TMouseEvent.java
src/jexer/io/Screen.java

index 1dd5790ebcad447854e56c9c770e063fc82c8157..c55438d32b371524fccd83ef8bef5be5e35fa952 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -41,6 +41,9 @@ TARGET_DIR = classes
 JEXER_SRC = $(SRC_DIR)/jexer/TApplication.java \
        $(SRC_DIR)/jexer/TCommand.java \
        $(SRC_DIR)/jexer/TKeypress.java \
+       $(SRC_DIR)/jexer/THScroller.java \
+       $(SRC_DIR)/jexer/TVScroller.java \
+       $(SRC_DIR)/jexer/TWidget.java \
        $(SRC_DIR)/jexer/bits/GraphicsChars.java \
        $(SRC_DIR)/jexer/bits/Color.java \
        $(SRC_DIR)/jexer/bits/CellAttributes.java \
@@ -65,6 +68,9 @@ JEXER_SRC = $(SRC_DIR)/jexer/TApplication.java \
 JEXER_BIN = $(TARGET_DIR)/jexer/TApplication.class \
        $(TARGET_DIR)/jexer/TCommand.class \
        $(TARGET_DIR)/jexer/TKeypress.class \
+       $(TARGET_DIR)/jexer/THScroller.class \
+       $(TARGET_DIR)/jexer/TVScroller.class \
+       $(TARGET_DIR)/jexer/TWidget.class \
        $(TARGET_DIR)/jexer/bits/GraphicsChars.class \
        $(TARGET_DIR)/jexer/bits/Color.class \
        $(TARGET_DIR)/jexer/bits/CellAttributes.class \
index 88249abc3541273ca2e4a686e3776f8dd9ed06f1..d4a6610834fa2994372ae3700efdf76a3c633718 100644 (file)
@@ -46,6 +46,7 @@ import jexer.event.TMouseEvent;
 import jexer.event.TResizeEvent;
 import jexer.backend.Backend;
 import jexer.backend.ECMA48Backend;
+import jexer.io.Screen;
 import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
@@ -59,6 +60,15 @@ public class TApplication {
      */
     private Backend backend;
 
+    /**
+     * Get the Screen.
+     *
+     * @return the Screen
+     */
+    public final Screen getScreen() {
+        return backend.getScreen();
+    }
+
     /**
      * Actual mouse coordinate X.
      */
@@ -91,29 +101,54 @@ public class TApplication {
     /**
      * When true, exit the application.
      */
-    public boolean quit = false;
+    private boolean quit = false;
 
     /**
      * When true, repaint the entire screen.
      */
-    public boolean repaint = true;
+    private boolean repaint = true;
+
+    /**
+     * Request full repaint on next screen refresh.
+     */
+    public void setRepaint() {
+        repaint = true;
+    }
 
     /**
      * When true, just flush updates from the screen.
      */
-    public boolean flush = false;
+    private boolean flush = false;
 
     /**
      * Y coordinate of the top edge of the desktop.  For now this is a
      * constant.  Someday it would be nice to have a multi-line menu or
      * toolbars.
      */
-    public static final int desktopTop = 1;
+    private static final int desktopTop = 1;
+
+    /**
+     * Get Y coordinate of the top edge of the desktop.
+     *
+     * @return Y coordinate of the top edge of the desktop
+     */
+    public final int getDesktopTop() {
+        return desktopTop;
+    }
 
     /**
      * Y coordinate of the bottom edge of the desktop.
      */
-    public int desktopBottom;
+    private int desktopBottom;
+
+    /**
+     * Get Y coordinate of the bottom edge of the desktop.
+     *
+     * @return Y coordinate of the bottom edge of the desktop
+     */
+    public final int getDesktopBottom() {
+        return desktopBottom;
+    }
 
     /**
      * Public constructor.
@@ -450,4 +485,136 @@ public class TApplication {
         return 250;
     }
 
+    /**
+     * Close window.  Note that the window's destructor is NOT called by this
+     * method, instead the GC is assumed to do the cleanup.
+     *
+     * @param window the window to remove
+     */
+    public final void closeWindow(final TWindow window) {
+        /*
+         TODO
+
+        uint z = window.z;
+        window.z = -1;
+        windows.sort;
+        windows = windows[1 .. $];
+        TWindow activeWindow = null;
+        foreach (w; windows) {
+            if (w.z > z) {
+                w.z--;
+                if (w.z == 0) {
+                    w.active = true;
+                    assert(activeWindow is null);
+                    activeWindow = w;
+                } else {
+                    w.active = false;
+                }
+            }
+        }
+
+        // Perform window cleanup
+        window.onClose();
+
+        // Refresh screen
+        repaint = true;
+
+        // Check if we are closing a TMessageBox or similar
+        if (secondaryEventReceiver !is null) {
+            assert(secondaryEventFiber !is null);
+
+            // Do not send events to the secondaryEventReceiver anymore, the
+            // window is closed.
+            secondaryEventReceiver = null;
+
+            // Special case: if this is called while executing on a
+            // secondaryEventFiber, call it so that widgetEventHandler() can
+            // terminate.
+            if (secondaryEventFiber.state == Fiber.State.HOLD) {
+                secondaryEventFiber.call();
+            }
+            secondaryEventFiber = null;
+
+            // Unfreeze the logic in handleEvent()
+            if (primaryEventFiber.state == Fiber.State.HOLD) {
+                primaryEventFiber.call();
+            }
+        }
+         */
+    }
+
+    /**
+     * Switch to the next window.
+     *
+     * @param forward if true, then switch to the next window in the list,
+     * otherwise switch to the previous window in the list
+     */
+    public final void switchWindow(final boolean forward) {
+        /*
+         TODO
+
+        // Only switch if there are multiple windows
+        if (windows.length < 2) {
+            return;
+        }
+
+        // Swap z/active between active window and the next in the
+        // list
+        ptrdiff_t activeWindowI = -1;
+        for (auto i = 0; i < windows.length; i++) {
+            if (windows[i].active) {
+                activeWindowI = i;
+                break;
+            }
+        }
+        assert(activeWindowI >= 0);
+
+        // Do not switch if a window is modal
+        if (windows[activeWindowI].isModal()) {
+            return;
+        }
+
+        size_t nextWindowI;
+        if (forward) {
+            nextWindowI = (activeWindowI + 1) % windows.length;
+        } else {
+            if (activeWindowI == 0) {
+                nextWindowI = windows.length - 1;
+            } else {
+                nextWindowI = activeWindowI - 1;
+            }
+        }
+        windows[activeWindowI].active = false;
+        windows[activeWindowI].z = windows[nextWindowI].z;
+        windows[nextWindowI].z = 0;
+        windows[nextWindowI].active = true;
+
+        // Refresh
+        repaint = true;
+        */
+    }
+
+    /**
+     * Add a window to my window list and make it active.
+     *
+     * @param window new window to add
+     */
+    public final void addWindow(final TWindow window) {
+        /*
+         TODO
+        // Do not allow a modal window to spawn a non-modal window
+        if ((windows.length > 0) && (windows[0].isModal())) {
+            assert(window.isModal());
+        }
+        foreach (w; windows) {
+            w.active = false;
+            w.z++;
+        }
+        windows ~= window;
+        window.active = true;
+        window.z = 0;
+         */
+    }
+
+
 }
diff --git a/src/jexer/THScroller.java b/src/jexer/THScroller.java
new file mode 100644 (file)
index 0000000..b705fe2
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Jexer - Java Text User Interface
+ *
+ * License: LGPLv3 or later
+ *
+ * This module is licensed under the GNU Lesser General Public License
+ * Version 3.  Please see the file "COPYING" in this directory for more
+ * information about the GNU Lesser General Public License Version 3.
+ *
+ *     Copyright (C) 2015  Kevin Lamonte
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, see
+ * http://www.gnu.org/licenses/, or write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+/**
+ * THScroller implements a simple horizontal scroll bar.
+ */
+public class THScroller extends TWidget {
+
+}
diff --git a/src/jexer/TMenu.java b/src/jexer/TMenu.java
new file mode 100644 (file)
index 0000000..b0873f0
--- /dev/null
@@ -0,0 +1,106 @@
+/**
+ * Jexer - Java Text User Interface
+ *
+ * License: LGPLv3 or later
+ *
+ * This module is licensed under the GNU Lesser General Public License
+ * Version 3.  Please see the file "COPYING" in this directory for more
+ * information about the GNU Lesser General Public License Version 3.
+ *
+ *     Copyright (C) 2015  Kevin Lamonte
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, see
+ * http://www.gnu.org/licenses/, or write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+import jexer.bits.MnemonicString;
+
+/**
+ * TMenu is a top-level collection of TMenuItems.
+ */
+public class TMenu extends TWindow {
+
+    /**
+     * If true, this is a sub-menu.
+     */
+    private boolean isSubMenu = false;
+
+    /**
+     * The shortcut and title.
+     */
+    private MnemonicString mnemonic;
+
+    // Reserved menu item IDs
+    public static final int MID_UNUSED          = -1;
+
+    // File menu
+    public static final int MID_EXIT            = 1;
+    public static final int MID_QUIT            = MID_EXIT;
+    public static final int MID_OPEN_FILE       = 2;
+    public static final int MID_SHELL           = 3;
+
+    // Edit menu
+    public static final int MID_CUT             = 10;
+    public static final int MID_COPY            = 11;
+    public static final int MID_PASTE           = 12;
+    public static final int MID_CLEAR           = 13;
+
+    // Window menu
+    public static final int MID_TILE            = 20;
+    public static final int MID_CASCADE         = 21;
+    public static final int MID_CLOSE_ALL       = 22;
+    public static final int MID_WINDOW_MOVE     = 23;
+    public static final int MID_WINDOW_ZOOM     = 24;
+    public static final int MID_WINDOW_NEXT     = 25;
+    public static final int MID_WINDOW_PREVIOUS = 26;
+    public static final int MID_WINDOW_CLOSE    = 27;
+
+    /**
+     * Public constructor.
+     *
+     * @param parent parent application
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param label mnemonic menu title.  Label must contain a keyboard
+     * shortcut (mnemonic), denoted by prefixing a letter with "&",
+     * e.g. "&File"
+     */
+    public TMenu(final TApplication parent, final int x, final int y,
+        final String label) {
+
+        super(parent, label, x, y, parent.getScreen().getWidth(),
+            parent.getScreen().getHeight());
+
+        // My parent constructor added me as a window, get rid of that
+        parent.closeWindow(this);
+
+        // Setup the menu shortcut
+        mnemonic = new MnemonicString(title);
+        this.title = mnemonic.getRawLabel();
+        assert (mnemonic.getShortcutIdx() >= 0);
+
+        // Recompute width and height to reflect an empty menu
+        width = this.title.length() + 4;
+        height = 2;
+
+        this.active = false;
+    }
+
+}
diff --git a/src/jexer/TVScroller.java b/src/jexer/TVScroller.java
new file mode 100644 (file)
index 0000000..50a08a1
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Jexer - Java Text User Interface
+ *
+ * License: LGPLv3 or later
+ *
+ * This module is licensed under the GNU Lesser General Public License
+ * Version 3.  Please see the file "COPYING" in this directory for more
+ * information about the GNU Lesser General Public License Version 3.
+ *
+ *     Copyright (C) 2015  Kevin Lamonte
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, see
+ * http://www.gnu.org/licenses/, or write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+/**
+ * TVScroller implements a simple vertical scroll bar.
+ */
+public class TVScroller extends TWidget {
+
+}
diff --git a/src/jexer/TWidget.java b/src/jexer/TWidget.java
new file mode 100644 (file)
index 0000000..6312d08
--- /dev/null
@@ -0,0 +1,720 @@
+/**
+ * Jexer - Java Text User Interface
+ *
+ * License: LGPLv3 or later
+ *
+ * This module is licensed under the GNU Lesser General Public License
+ * Version 3.  Please see the file "COPYING" in this directory for more
+ * information about the GNU Lesser General Public License Version 3.
+ *
+ *     Copyright (C) 2015  Kevin Lamonte
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, see
+ * http://www.gnu.org/licenses/, or write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+import java.util.List;
+import java.util.LinkedList;
+
+import jexer.event.TCommandEvent;
+import jexer.event.TInputEvent;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMenuEvent;
+import jexer.event.TMouseEvent;
+import jexer.event.TResizeEvent;
+import jexer.io.Screen;
+import static jexer.TKeypress.*;
+
+/**
+ * TWidget is the base class of all objects that can be drawn on screen or
+ * handle user input events.
+ */
+public abstract class TWidget {
+
+    /**
+     * Every widget has a parent widget that it may be "contained" in.  For
+     * example, a TWindow might contain several TTextFields, or a TComboBox
+     * may contain a TScrollBar.
+     */
+    protected TWidget parent = null;
+
+    /**
+     * Child widgets that this widget contains.
+     */
+    private List<TWidget> children;
+
+    /**
+     * The currently active child widget that will receive keypress events.
+     */
+    private TWidget activeChild = null;
+
+    /**
+     * If true, this widget will receive events.
+     */
+    protected boolean active = false;
+
+    /**
+     * The window that this widget draws to.
+     */
+    protected TWindow window = null;
+
+    /**
+     * Absolute X position of the top-left corner.
+     */
+    protected int x = 0;
+
+    /**
+     * Absolute Y position of the top-left corner.
+     */
+    protected int y = 0;
+
+    /**
+     * Width.
+     */
+    protected int width = 0;
+
+    /**
+     * Height.
+     */
+    protected int height = 0;
+
+    /**
+     * My tab order inside a window or containing widget.
+     */
+    private int tabOrder = 0;
+
+    /**
+     * If true, this widget can be tabbed to or receive events.
+     */
+    private boolean enabled = true;
+
+    /**
+     * Get enabled flag.
+     *
+     * @return if true, this widget can be tabbed to or receive events
+     */
+    public final boolean getEnabled() {
+        return enabled;
+    }
+
+    /**
+     * Set enabled flag.
+     *
+     * @param enabled if true, this widget can be tabbed to or receive events
+     */
+    public final void setEnabled(final boolean enabled) {
+        this.enabled = enabled;
+        /*
+
+        // TODO: get this working after scrollers are going again
+
+        if (enabled == false) {
+            active = false;
+            // See if there are any active siblings to switch to
+            boolean foundSibling = false;
+            if (parent !is null) {
+                foreach (w; parent.children) {
+                    if ((w.enabled) &&
+                        (!cast(THScroller)this) &&
+                        (!cast(TVScroller)this)
+                    ) {
+                        parent.activate(w);
+                        foundSibling = true;
+                        break;
+                    }
+                }
+                if (!foundSibling) {
+                    parent.activeChild = null;
+                }
+            }
+        }
+         */
+    }
+
+    /**
+     * If true, this widget has a cursor.
+     */
+    private boolean hasCursor = false;
+
+    /**
+     * Cursor column position in relative coordinates.
+     */
+    private int cursorX = 0;
+
+    /**
+     * Cursor row position in relative coordinates.
+     */
+    private int cursorY = 0;
+
+    /**
+     * Comparison operator sorts on tabOrder.
+     *
+     * @param that another TWidget instance
+     * @return difference between this.tabOrder and that.tabOrder
+     */
+    public final int compare(final TWidget that) {
+        return (this.tabOrder - that.tabOrder);
+    }
+
+    /**
+     * See if this widget should render with the active color.
+     *
+     * @return true if this widget is active and all of its parents are
+     * active.
+     */
+    public final boolean getAbsoluteActive() {
+        if (parent == this) {
+            return active;
+        }
+        return (active && parent.getAbsoluteActive());
+    }
+
+    /**
+     * Returns the cursor X position.
+     *
+     * @return absolute screen column number for the cursor's X position
+     */
+    public final int getCursorAbsoluteX() {
+        assert (hasCursor);
+        return getAbsoluteX() + cursorX;
+    }
+
+    /**
+     * Returns the cursor Y position.
+     *
+     * @return absolute screen row number for the cursor's Y position
+     */
+    public final int getCursorAbsoluteY() {
+        assert (hasCursor);
+        return getAbsoluteY() + cursorY;
+    }
+
+    /**
+     * Compute my absolute X position as the sum of my X plus all my parent's
+     * X's.
+     *
+     * @return absolute screen column number for my X position
+     */
+    public final int getAbsoluteX() {
+        assert (parent != null);
+        if (parent == this) {
+            return x;
+        }
+        if ((parent instanceof TWindow) && !(parent instanceof TMenu)) {
+            // Widgets on a TWindow have (0,0) as their top-left, but this is
+            // actually the TWindow's (1,1).
+            return parent.getAbsoluteX() + x + 1;
+        }
+        return parent.getAbsoluteX() + x;
+    }
+
+    /**
+     * Compute my absolute Y position as the sum of my Y plus all my parent's
+     * Y's.
+     *
+     * @return absolute screen row number for my Y position
+     */
+    public final int getAbsoluteY() {
+        assert (parent != null);
+        if (parent == this) {
+            return y;
+        }
+        if ((parent instanceof TWindow) && !(parent instanceof TMenu)) {
+            // Widgets on a TWindow have (0,0) as their top-left, but this is
+            // actually the TWindow's (1,1).
+            return parent.getAbsoluteY() + y + 1;
+        }
+        return parent.getAbsoluteY() + y;
+    }
+
+    /**
+     * Draw my specific widget.  When called, the screen rectangle I draw
+     * into is already setup (offset and clipping).
+     */
+    public void draw() {
+        // Default widget draws nothing.
+    }
+
+    /**
+     * Called by parent to render to TWindow.
+     */
+    public final void drawChildren() {
+        // Set my clipping rectangle
+        assert (window != null);
+        assert (window.getScreen() != null);
+        Screen screen = window.getScreen();
+
+        screen.setClipRight(width);
+        screen.setClipBottom(height);
+
+        int absoluteRightEdge = window.getAbsoluteX() + screen.getWidth();
+        int absoluteBottomEdge = window.getAbsoluteY() + screen.getHeight();
+        if (!(this instanceof TWindow) && !(this instanceof TVScroller)) {
+            absoluteRightEdge -= 1;
+        }
+        if (!(this instanceof TWindow) && !(this instanceof THScroller)) {
+            absoluteBottomEdge -= 1;
+        }
+        int myRightEdge = getAbsoluteX() + width;
+        int myBottomEdge = getAbsoluteY() + height;
+        if (getAbsoluteX() > absoluteRightEdge) {
+            // I am offscreen
+            screen.setClipRight(0);
+        } else if (myRightEdge > absoluteRightEdge) {
+            screen.setClipRight(screen.getClipRight()
+                - myRightEdge - absoluteRightEdge);
+        }
+        if (getAbsoluteY() > absoluteBottomEdge) {
+            // I am offscreen
+            screen.setClipBottom(0);
+        } else if (myBottomEdge > absoluteBottomEdge) {
+            screen.setClipBottom(screen.getClipBottom()
+                - myBottomEdge - absoluteBottomEdge);
+        }
+
+        // Set my offset
+        screen.setOffsetX(getAbsoluteX());
+        screen.setOffsetY(getAbsoluteY());
+
+        // Draw me
+        draw();
+
+        // Continue down the chain
+        for (TWidget widget: children) {
+            widget.drawChildren();
+        }
+    }
+
+    /**
+     * Subclasses need this constructor to setup children.
+     */
+    protected TWidget() {
+        children = new LinkedList<TWidget>();
+    }
+
+    /**
+     * Protected constructor.
+     *
+     * @param parent parent widget
+     */
+    protected TWidget(final TWidget parent) {
+        this.parent = parent;
+        this.window = parent.window;
+
+        parent.addChild(this);
+    }
+
+    /**
+     * Add a child widget to my list of children.  We set its tabOrder to 0
+     * and increment the tabOrder of all other children.
+     *
+     * @param child TWidget to add
+     */
+    private void addChild(final TWidget child) {
+        children.add(child);
+
+        if ((child.enabled)
+            && !(child instanceof THScroller)
+            && !(child instanceof TVScroller)
+        ) {
+            for (TWidget widget: children) {
+                widget.active = false;
+            }
+            child.active = true;
+            activeChild = child;
+        }
+        for (int i = 0; i < children.size(); i++) {
+            children.get(i).tabOrder = i;
+        }
+    }
+
+    /**
+     * Switch the active child.
+     *
+     * @param child TWidget to activate
+     */
+    public final void activate(final TWidget child) {
+        assert (child.enabled);
+        if ((child instanceof THScroller)
+            || (child instanceof TVScroller)
+        ) {
+            return;
+        }
+
+        if (child != activeChild) {
+            if (activeChild != null) {
+                activeChild.active = false;
+            }
+            child.active = true;
+            activeChild = child;
+        }
+    }
+
+    /**
+     * Switch the active child.
+     *
+     * @param tabOrder tabOrder of the child to activate.  If that child
+     * isn't enabled, then the next enabled child will be activated.
+     */
+    public final void activate(final int tabOrder) {
+        if (activeChild == null) {
+            return;
+        }
+        TWidget child = null;
+        for (TWidget widget: children) {
+            if ((widget.enabled)
+                && !(widget instanceof THScroller)
+                && !(widget instanceof TVScroller)
+                && (widget.tabOrder >= tabOrder)
+            ) {
+                child = widget;
+                break;
+            }
+        }
+        if ((child != null) && (child != activeChild)) {
+            activeChild.active = false;
+            assert (child.enabled);
+            child.active = true;
+            activeChild = child;
+        }
+    }
+
+    /**
+     * Switch the active widget with the next in the tab order.
+     *
+     * @param forward if true, then switch to the next enabled widget in the
+     * list, otherwise switch to the previous enabled widget in the list
+     */
+    public final void switchWidget(final boolean forward) {
+
+        // Only switch if there are multiple enabled widgets
+        if ((children.size() < 2) || (activeChild == null)) {
+            return;
+        }
+
+        int tabOrder = activeChild.tabOrder;
+        do {
+            if (forward) {
+                tabOrder++;
+            } else {
+                tabOrder--;
+            }
+            if (tabOrder < 0) {
+
+                // If at the end, pass the switch to my parent.
+                if ((!forward) && (parent != this)) {
+                    parent.switchWidget(forward);
+                    return;
+                }
+
+                tabOrder = children.size() - 1;
+            } else if (tabOrder == children.size()) {
+                // If at the end, pass the switch to my parent.
+                if ((forward) && (parent != this)) {
+                    parent.switchWidget(forward);
+                    return;
+                }
+
+                tabOrder = 0;
+            }
+            if (activeChild.tabOrder == tabOrder) {
+                // We wrapped around
+                break;
+            }
+        } while ((!children.get(tabOrder).enabled)
+            && !(children.get(tabOrder) instanceof THScroller)
+            && !(children.get(tabOrder) instanceof TVScroller));
+
+        assert (children.get(tabOrder).enabled);
+
+        activeChild.active = false;
+        children.get(tabOrder).active = true;
+        activeChild = children.get(tabOrder);
+
+        // Refresh
+        window.getApplication().setRepaint();
+    }
+
+    /**
+     * Returns my active widget.
+     *
+     * @return widget that is active, or this if no children
+     */
+    public final TWidget getActiveChild() {
+        if ((this instanceof THScroller)
+            || (this instanceof TVScroller)
+        ) {
+            return parent;
+        }
+
+        for (TWidget widget: children) {
+            if (widget.active) {
+                return widget.getActiveChild();
+            }
+        }
+        // No active children, return me
+        return this;
+    }
+
+    /**
+     * Method that subclasses can override to handle keystrokes.
+     *
+     * @param keypress keystroke event
+     */
+    public void onKeypress(final TKeypressEvent keypress) {
+
+        if ((children.size() == 0)
+            // || (cast(TTreeView)this)
+            // || (cast(TText)this)
+        ) {
+
+            // Defaults:
+            //   tab / shift-tab - switch to next/previous widget
+            //   right-arrow or down-arrow: same as tab
+            //   left-arrow or up-arrow: same as shift-tab
+            if ((keypress.equals(kbTab))
+                || (keypress.equals(kbRight))
+                || (keypress.equals(kbDown))
+            ) {
+                parent.switchWidget(true);
+                return;
+            } else if ((keypress.equals(kbShiftTab))
+                || (keypress.equals(kbBackTab))
+                || (keypress.equals(kbLeft))
+                || (keypress.equals(kbUp))
+            ) {
+                parent.switchWidget(false);
+                return;
+            }
+        }
+
+        // If I have any buttons on me AND this is an Alt-key that matches
+        // its mnemonic, send it an Enter keystroke
+        for (TWidget widget: children) {
+            /*
+            TODO
+
+            if (TButton button = cast(TButton)w) {
+                if (button.enabled &&
+                    !keypress.key.isKey &&
+                    keypress.key.alt &&
+                    !keypress.key.ctrl &&
+                    (toLowercase(button.mnemonic.shortcut) == toLowercase(keypress.key.ch))) {
+
+                    w.handleEvent(new TKeypressEvent(kbEnter));
+                    return;
+                }
+            }
+             */
+        }
+
+        // Dispatch the keypress to an active widget
+        for (TWidget widget: children) {
+            if (widget.active) {
+                window.getApplication().setRepaint();
+                widget.handleEvent(keypress);
+                return;
+            }
+        }
+    }
+
+    /**
+     * Method that subclasses can override to handle mouse button presses.
+     *
+     * @param mouse mouse button event
+     */
+    public void onMouseDown(final TMouseEvent mouse) {
+        // Default: do nothing, pass to children instead
+        for (TWidget widget: children) {
+            if (widget.mouseWouldHit(mouse)) {
+                // Dispatch to this child, also activate it
+                activate(widget);
+
+                // Set x and y relative to the child's coordinates
+                mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+                mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+                widget.handleEvent(mouse);
+                return;
+            }
+        }
+    }
+
+    /**
+     * Method that subclasses can override to handle mouse button releases.
+     *
+     * @param mouse mouse button event
+     */
+    public void onMouseUp(final TMouseEvent mouse) {
+        // Default: do nothing, pass to children instead
+        for (TWidget widget: children) {
+            if (widget.mouseWouldHit(mouse)) {
+                // Dispatch to this child, also activate it
+                activate(widget);
+
+                // Set x and y relative to the child's coordinates
+                mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+                mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+                widget.handleEvent(mouse);
+                return;
+            }
+        }
+    }
+
+    /**
+     * Method that subclasses can override to handle mouse movements.
+     *
+     * @param mouse mouse motion event
+     */
+    public void onMouseMotion(final TMouseEvent mouse) {
+        // Default: do nothing, pass it on to ALL of my children.  This way
+        // the children can see the mouse "leaving" their area.
+        for (TWidget widget: children) {
+            // Set x and y relative to the child's coordinates
+            mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+            mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+            widget.handleEvent(mouse);
+        }
+    }
+
+    /**
+     * Method that subclasses can override to handle window/screen resize
+     * events.
+     *
+     * @param resize resize event
+     */
+    public void onResize(final TResizeEvent resize) {
+        // Default: do nothing, pass to children instead
+        for (TWidget widget: children) {
+            widget.onResize(resize);
+        }
+    }
+
+    /**
+     * Method that subclasses can override to handle posted command events.
+     *
+     * @param command command event
+     */
+    public void onCommand(final TCommandEvent command) {
+        // Default: do nothing, pass to children instead
+        for (TWidget widget: children) {
+            widget.onCommand(command);
+        }
+    }
+
+    /**
+     * Method that subclasses can override to handle menu or posted menu
+     * events.
+     *
+     * @param menu menu event
+     */
+    public void onMenu(final TMenuEvent menu) {
+        // Default: do nothing, pass to children instead
+        for (TWidget widget: children) {
+            widget.onMenu(menu);
+        }
+    }
+
+    /**
+     * Method that subclasses can override to do processing when the UI is
+     * idle.
+     */
+    public void onIdle() {
+        // Default: do nothing, pass to children instead
+        for (TWidget widget: children) {
+            widget.onIdle();
+        }
+    }
+
+    /**
+     * Consume event.  Subclasses that want to intercept all events in one go
+     * can override this method.
+     *
+     * @param event keyboard, mouse, resize, command, or menu event
+     */
+    public void handleEvent(final TInputEvent event) {
+        // System.err.printf("TWidget (%s) event: %s\n", this.getClass().getName(),
+        //     event);
+
+        if (!enabled) {
+            // Discard event
+            // System.err.println("   -- discard --");
+            return;
+        }
+
+        if (event instanceof TKeypressEvent) {
+            onKeypress((TKeypressEvent) event);
+        } else if (event instanceof TMouseEvent) {
+
+            TMouseEvent mouse = (TMouseEvent) event;
+
+            switch (mouse.getType()) {
+
+            case MOUSE_DOWN:
+                onMouseDown(mouse);
+                break;
+
+            case MOUSE_UP:
+                onMouseUp(mouse);
+                break;
+
+            case MOUSE_MOTION:
+                onMouseMotion(mouse);
+                break;
+
+            default:
+                throw new IllegalArgumentException("Invalid mouse event type: "
+                    + mouse.getType());
+            }
+        } else if (event instanceof TResizeEvent) {
+            onResize((TResizeEvent) event);
+        } else if (event instanceof TCommandEvent) {
+            onCommand((TCommandEvent) event);
+        } else if (event instanceof TMenuEvent) {
+            onMenu((TMenuEvent) event);
+        }
+
+        // Do nothing else
+        return;
+    }
+
+    /**
+     * Check if a mouse press/release event coordinate is contained in this
+     * widget.
+     *
+     * @param mouse a mouse-based event
+     * @return whether or not a mouse click would be sent to this widget
+     */
+    public final boolean mouseWouldHit(final TMouseEvent mouse) {
+
+        if (!enabled) {
+            return false;
+        }
+
+        if ((mouse.getAbsoluteX() >= getAbsoluteX())
+            && (mouse.getAbsoluteX() <  getAbsoluteX() + width)
+            && (mouse.getAbsoluteY() >= getAbsoluteY())
+            && (mouse.getAbsoluteY() <  getAbsoluteY() + height)
+        ) {
+            return true;
+        }
+        return false;
+    }
+
+}
diff --git a/src/jexer/TWindow.java b/src/jexer/TWindow.java
new file mode 100644 (file)
index 0000000..b850789
--- /dev/null
@@ -0,0 +1,1000 @@
+/**
+ * Jexer - Java Text User Interface
+ *
+ * License: LGPLv3 or later
+ *
+ * This module is licensed under the GNU Lesser General Public License
+ * Version 3.  Please see the file "COPYING" in this directory for more
+ * information about the GNU Lesser General Public License Version 3.
+ *
+ *     Copyright (C) 2015  Kevin Lamonte
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, see
+ * http://www.gnu.org/licenses/, or write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+import jexer.bits.Cell;
+import jexer.bits.CellAttributes;
+import jexer.bits.GraphicsChars;
+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 static jexer.TCommand.*;
+import static jexer.TKeypress.*;
+
+/**
+ * TWindow is the top-level container and drawing surface for other widgets.
+ */
+public class TWindow extends TWidget {
+
+    /**
+     * Window's parent TApplication.
+     */
+    protected TApplication application;
+
+    /**
+     * Get this TWindow's parent TApplication.
+     *
+     * @return this TWindow's parent TApplication
+     */
+    public final TApplication getApplication() {
+        return application;
+    }
+
+    /**
+     * Get the Screen.
+     *
+     * @return the Screen
+     */
+    public final Screen getScreen() {
+        return application.getScreen();
+    }
+
+    /**
+     * Window title.
+     */
+    protected String title = "";
+
+    /**
+     * Window is resizable (default yes).
+     */
+    public static final int RESIZABLE   = 0x01;
+
+    /**
+     * Window is modal (default no).
+     */
+    public static final int MODAL       = 0x02;
+
+    /**
+     * Window is centered (default no).
+     */
+    public static final int CENTERED    = 0x04;
+
+    /**
+     * Window flags.
+     */
+    private int flags = RESIZABLE;
+
+    /**
+     * Z order.  Lower number means more in-front.
+     */
+    private int z = 0;
+
+    /**
+     * If true, then the user clicked on the title bar and is moving the
+     * window.
+     */
+    private boolean inWindowMove = false;
+
+    /**
+     * If true, then the user clicked on the bottom right corner and is
+     * resizing the window.
+     */
+    private boolean inWindowResize = false;
+
+    /**
+     * 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;
+
+    /**
+     * If true, this window is maximized.
+     */
+    private boolean maximized = false;
+
+    /**
+     * Remember mouse state.
+     */
+    protected TMouseEvent mouse;
+
+    // For moving the window.  resizing also uses moveWindowMouseX/Y
+    private int moveWindowMouseX;
+    private int moveWindowMouseY;
+    private int oldWindowX;
+    private int oldWindowY;
+
+    // Resizing
+    private int resizeWindowWidth;
+    private int resizeWindowHeight;
+    private int minimumWindowWidth = 10;
+    private int minimumWindowHeight = 2;
+    private int maximumWindowWidth = -1;
+    private int maximumWindowHeight = -1;
+
+    // For maximize/restore
+    private int restoreWindowWidth;
+    private int restoreWindowHeight;
+    private int restoreWindowX;
+    private int restoreWindowY;
+
+    /**
+     * Public constructor.  Window will be located at (0, 0).
+     *
+     * @param application TApplication that manages this window
+     * @param title window title, will be centered along the top border
+     * @param width width of window
+     * @param height height of window
+     */
+    public TWindow(final TApplication application, final String title,
+        final int width, final int height) {
+
+        this(application, title, 0, 0, width, height, RESIZABLE);
+    }
+
+    /**
+     * Public constructor.  Window will be located at (0, 0).
+     *
+     * @param application TApplication that manages this window
+     * @param title window title, will be centered along the top border
+     * @param width width of window
+     * @param height height of window
+     * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
+     */
+    public TWindow(final TApplication application, final String title,
+        final int width, final int height, final int flags) {
+
+        this(application, title, 0, 0, width, height, flags);
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param application TApplication that manages this window
+     * @param title window title, will be centered along the top border
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of window
+     * @param height height of window
+     */
+    public TWindow(final TApplication application, final String title,
+        final int x, final int y, final int width, final int height) {
+
+        this(application, title, x, y, width, height, RESIZABLE);
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param application TApplication that manages this window
+     * @param title window title, will be centered along the top border
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of window
+     * @param height height of window
+     * @param flags mask of RESIZABLE, CENTERED, or MODAL
+     */
+    public TWindow(final TApplication application, final String title,
+        final int x, final int y, final int width, final int height,
+        final int flags) {
+
+        // I am my own window and parent
+        this.parent = this;
+        this.window = this;
+
+        // Save fields
+        this.title       = title;
+        this.application = application;
+        this.x           = x;
+        this.y           = y + application.getDesktopTop();
+        this.width       = width;
+        this.height      = height;
+        this.flags       = flags;
+
+        // Minimum width/height are 10 and 2
+        assert (width >= 10);
+        assert (height >= 2);
+
+        // MODAL implies CENTERED
+        if (isModal()) {
+            this.flags |= CENTERED;
+        }
+
+        // Center window if specified
+        center();
+
+        // Add me to the application
+        application.addWindow(this);
+    }
+
+    /**
+     * Recenter the window on-screen.
+     */
+    public final void center() {
+        if ((flags & CENTERED) != 0) {
+            if (width < getScreen().getWidth()) {
+                x = (getScreen().getWidth() - width) / 2;
+            } else {
+                x = 0;
+            }
+            y = (application.getDesktopBottom() - application.getDesktopTop());
+            y -= height;
+            y /= 2;
+            if (y < 0) {
+                y = 0;
+            }
+            y += application.getDesktopTop();
+        }
+    }
+
+    /**
+     * Returns true if this window is modal.
+     *
+     * @return true if this window is modal
+     */
+    public final boolean isModal() {
+        if ((flags & MODAL) == 0) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Comparison operator sorts on z.
+     *
+     * @param that another TWindow instance
+     * @return difference between this.z and that.z
+     */
+    public final int compare(final TWindow that) {
+        return (z - that.z);
+    }
+
+    /**
+     * Returns true if the mouse is currently on the close button.
+     *
+     * @return true if mouse is currently on the close button
+     */
+    private boolean mouseOnClose() {
+        if ((mouse != null)
+            && (mouse.getAbsoluteY() == y)
+            && (mouse.getAbsoluteX() == x + 3)
+        ) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if the mouse is currently on the maximize/restore button.
+     *
+     * @return true if the mouse is currently on the maximize/restore button
+     */
+    private boolean mouseOnMaximize() {
+        if ((mouse != null)
+            && !isModal()
+            && (mouse.getAbsoluteY() == y)
+            && (mouse.getAbsoluteX() == x + width - 4)
+        ) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if the mouse is currently on the resizable lower right
+     * corner.
+     *
+     * @return true if the mouse is currently on the resizable lower right
+     * corner
+     */
+    private boolean mouseOnResize() {
+        if (((flags & RESIZABLE) != 0)
+            && !isModal()
+            && (mouse != null)
+            && (mouse.getAbsoluteY() == y + height - 1)
+            && ((mouse.getAbsoluteX() == x + width - 1)
+                || (mouse.getAbsoluteX() == x + width - 2))
+        ) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Retrieve the background color.
+     *
+     * @return the background color
+     */
+    protected final CellAttributes getBackground() {
+        if (!isModal()
+            && (inWindowMove || inWindowResize || inKeyboardResize)
+        ) {
+            assert (active);
+            return application.getTheme().getColor("twindow.background.windowmove");
+        } else if (isModal() && inWindowMove) {
+            assert (active);
+            return application.getTheme().getColor("twindow.background.modal");
+        } else if (isModal()) {
+            if (active) {
+                return application.getTheme().getColor("twindow.background.modal");
+            }
+            return application.getTheme().getColor("twindow.background.modal.inactive");
+        } else if (active) {
+            assert (!isModal());
+            return application.getTheme().getColor("twindow.background");
+        } else {
+            assert (!isModal());
+            return application.getTheme().getColor("twindow.background.inactive");
+        }
+    }
+
+    /**
+     * Retrieve the border color.
+     *
+     * @return the border color
+     */
+    protected final CellAttributes getBorder() {
+        if (!isModal()
+            && (inWindowMove || inWindowResize || inKeyboardResize)
+        ) {
+            assert (active);
+            return application.getTheme().getColor("twindow.border.windowmove");
+        } else if (isModal() && inWindowMove) {
+            assert (active);
+            return application.getTheme().getColor("twindow.border.modal.windowmove");
+        } else if (isModal()) {
+            if (active) {
+                return application.getTheme().getColor("twindow.border.modal");
+            } else {
+                return application.getTheme().getColor("twindow.border.modal.inactive");
+            }
+        } else if (active) {
+            assert (!isModal());
+            return application.getTheme().getColor("twindow.border");
+        } else {
+            assert (!isModal());
+            return application.getTheme().getColor("twindow.border.inactive");
+        }
+    }
+
+    /**
+     * Retrieve the border line type.
+     *
+     * @return the border line type
+     */
+    protected final int getBorderType() {
+        if (!isModal()
+            && (inWindowMove || inWindowResize || inKeyboardResize)
+        ) {
+            assert (active);
+            return 1;
+        } else if (isModal() && inWindowMove) {
+            assert (active);
+            return 1;
+        } else if (isModal()) {
+            if (active) {
+                return 2;
+            } else {
+                return 1;
+            }
+        } else if (active) {
+            return 2;
+        } else {
+            return 1;
+        }
+    }
+
+    /**
+     * Subclasses should override this method to cleanup resources.  This is
+     * called by application.closeWindow().
+     */
+    public void onClose() {
+        // Default: do nothing
+    }
+
+    /**
+     * 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, width, height, border,
+            background, borderType, true);
+
+        // Draw the title
+        int titleLeft = (width - title.length() - 2) / 2;
+        putCharXY(titleLeft, 0, ' ', border);
+        putStrXY(titleLeft + 1, 0, title);
+        putCharXY(titleLeft + title.length() + 1, 0, ' ', border);
+
+        if (active) {
+
+            // Draw the close button
+            putCharXY(2, 0, '[', border);
+            putCharXY(4, 0, ']', border);
+            if (mouseOnClose() && mouse.getMouse1()) {
+                putCharXY(3, 0, GraphicsChars.CP437[0x0F],
+                    !isModal()
+                    ? application.getTheme().getColor("twindow.border.windowmove")
+                    : application.getTheme().getColor("twindow.border.modal.windowmove"));
+            } else {
+                putCharXY(3, 0, GraphicsChars.CP437[0xFE],
+                    !isModal()
+                    ? application.getTheme().getColor("twindow.border.windowmove")
+                    : application.getTheme().getColor("twindow.border.modal.windowmove"));
+            }
+
+            // Draw the maximize button
+            if (!isModal()) {
+
+                putCharXY(width - 5, 0, '[', border);
+                putCharXY(width - 3, 0, ']', border);
+                if (mouseOnMaximize() && mouse.getMouse1()) {
+                    putCharXY(width - 4, 0, GraphicsChars.CP437[0x0F],
+                        application.getTheme().getColor("twindow.border.windowmove"));
+                } else {
+                    if (maximized) {
+                        putCharXY(width - 4, 0, GraphicsChars.CP437[0x12],
+                            application.getTheme().getColor("twindow.border.windowmove"));
+                    } else {
+                        putCharXY(width - 4, 0, GraphicsChars.UPARROW,
+                            application.getTheme().getColor("twindow.border.windowmove"));
+                    }
+                }
+
+                // Draw the resize corner
+                if ((flags & RESIZABLE) != 0) {
+                    putCharXY(width - 2, height - 1, GraphicsChars.SINGLE_BAR,
+                        application.getTheme().getColor("twindow.border.windowmove"));
+                    putCharXY(width - 1, height - 1, GraphicsChars.LRCORNER,
+                        application.getTheme().getColor("twindow.border.windowmove"));
+                }
+            }
+        }
+    }
+
+    /**
+     * Handle mouse button presses.
+     *
+     * @param mouse mouse button event
+     */
+    @Override
+    public void onMouseDown(final TMouseEvent mouse) {
+        this.mouse = mouse;
+        application.setRepaint();
+
+        inKeyboardResize = false;
+
+        if ((mouse.getAbsoluteY() == y)
+            && mouse.getMouse1()
+            && (x <= mouse.getAbsoluteX())
+            && (mouse.getAbsoluteX() < x + width)
+            && !mouseOnClose()
+            && !mouseOnMaximize()
+        ) {
+            // Begin moving window
+            inWindowMove = true;
+            moveWindowMouseX = mouse.getAbsoluteX();
+            moveWindowMouseY = mouse.getAbsoluteY();
+            oldWindowX = x;
+            oldWindowY = y;
+            if (maximized) {
+                maximized = false;
+            }
+            return;
+        }
+        if (mouseOnResize()) {
+            // Begin window resize
+            inWindowResize = true;
+            moveWindowMouseX = mouse.getAbsoluteX();
+            moveWindowMouseY = mouse.getAbsoluteY();
+            resizeWindowWidth = width;
+            resizeWindowHeight = height;
+            if (maximized) {
+                maximized = false;
+            }
+            return;
+        }
+
+        // I didn't take it, pass it on to my children
+        super.onMouseDown(mouse);
+    }
+
+    /**
+     * Maximize window.
+     */
+    private void maximize() {
+        restoreWindowWidth = width;
+        restoreWindowHeight = height;
+        restoreWindowX = x;
+        restoreWindowY = y;
+        width = getScreen().getWidth();
+        height = application.getDesktopBottom() - 1;
+        x = 0;
+        y = 1;
+        maximized = true;
+    }
+
+    /**
+     * Restote (unmaximize) window.
+     */
+    private void restore() {
+        width = restoreWindowWidth;
+        height = restoreWindowHeight;
+        x = restoreWindowX;
+        y = restoreWindowY;
+        maximized = false;
+    }
+
+    /**
+     * Handle mouse button releases.
+     *
+     * @param mouse mouse button release event
+     */
+    @Override
+    public void onMouseUp(final TMouseEvent mouse) {
+        this.mouse = mouse;
+        application.setRepaint();
+
+        if ((inWindowMove) && (mouse.getMouse1())) {
+            // Stop moving window
+            inWindowMove = false;
+            return;
+        }
+
+        if ((inWindowResize) && (mouse.getMouse1())) {
+            // Stop resizing window
+            inWindowResize = false;
+            return;
+        }
+
+        if (mouse.getMouse1() && mouseOnClose()) {
+            // Close window
+            application.closeWindow(this);
+            return;
+        }
+
+        if ((mouse.getAbsoluteY() == y) && mouse.getMouse1()
+            && mouseOnMaximize()) {
+            if (maximized) {
+                // Restore
+                restore();
+            } else {
+                // Maximize
+                maximize();
+            }
+            // Pass a resize event to my children
+            onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, width, height));
+            return;
+        }
+
+        // I didn't take it, pass it on to my children
+        super.onMouseUp(mouse);
+    }
+
+    /**
+     * Handle mouse movements.
+     *
+     * @param mouse mouse motion event
+     */
+    @Override
+    public void onMouseMotion(final TMouseEvent mouse) {
+        this.mouse = mouse;
+        application.setRepaint();
+
+        if (inWindowMove) {
+            // Move window over
+            x = oldWindowX + (mouse.getAbsoluteX() - moveWindowMouseX);
+            y = oldWindowY + (mouse.getAbsoluteY() - moveWindowMouseY);
+            // Don't cover up the menu bar
+            if (y < application.getDesktopTop()) {
+                y = application.getDesktopTop();
+            }
+            return;
+        }
+
+        if (inWindowResize) {
+            // Move window over
+            width = resizeWindowWidth + (mouse.getAbsoluteX() - moveWindowMouseX);
+            height = resizeWindowHeight + (mouse.getAbsoluteY() - moveWindowMouseY);
+            if (x + width > getScreen().getWidth()) {
+                width = getScreen().getWidth() - x;
+            }
+            if (y + height > application.getDesktopBottom()) {
+                y = application.getDesktopBottom() - height + 1;
+            }
+            // Don't cover up the menu bar
+            if (y < application.getDesktopTop()) {
+                y = application.getDesktopTop();
+            }
+
+            // Keep within min/max bounds
+            if (width < minimumWindowWidth) {
+                width = minimumWindowWidth;
+                inWindowResize = false;
+            }
+            if (height < minimumWindowHeight) {
+                height = minimumWindowHeight;
+                inWindowResize = false;
+            }
+            if ((maximumWindowWidth > 0) && (width > maximumWindowWidth)) {
+                width = maximumWindowWidth;
+                inWindowResize = false;
+            }
+            if ((maximumWindowHeight > 0) && (height > maximumWindowHeight)) {
+                height = maximumWindowHeight;
+                inWindowResize = false;
+            }
+
+            // Pass a resize event to my children
+            onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, width, height));
+            return;
+        }
+
+        // I didn't take it, pass it on to my children
+        super.onMouseMotion(mouse);
+    }
+
+    /**
+     * Handle keystrokes.
+     *
+     * @param keypress keystroke event
+     */
+    @Override
+    public void onKeypress(final TKeypressEvent keypress) {
+
+        if (inKeyboardResize) {
+
+            // ESC - Exit size/move
+            if (keypress.equals(kbEsc)) {
+                inKeyboardResize = false;
+            }
+
+            if (keypress.equals(kbLeft)) {
+                if (x > 0) {
+                    x--;
+                }
+            }
+            if (keypress.equals(kbRight)) {
+                if (x < getScreen().getWidth() - 1) {
+                    x++;
+                }
+            }
+            if (keypress.equals(kbDown)) {
+                if (y < application.getDesktopBottom() - 1) {
+                    y++;
+                }
+            }
+            if (keypress.equals(kbUp)) {
+                if (y > 1) {
+                    y--;
+                }
+            }
+            if (keypress.equals(kbShiftLeft)) {
+                if (width > minimumWindowWidth) {
+                    width--;
+                }
+            }
+            if (keypress.equals(kbShiftRight)) {
+                if (width < maximumWindowWidth) {
+                    width++;
+                }
+            }
+            if (keypress.equals(kbShiftUp)) {
+                if (height > minimumWindowHeight) {
+                    height--;
+                }
+            }
+            if (keypress.equals(kbShiftDown)) {
+                if (height < maximumWindowHeight) {
+                    height++;
+                }
+            }
+
+            return;
+        }
+
+        // These keystrokes will typically not be seen unless a subclass
+        // overrides onMenu() due to how TApplication dispatches
+        // accelerators.
+
+        // Ctrl-W - close window
+        if (keypress.equals(kbCtrlW)) {
+            application.closeWindow(this);
+            return;
+        }
+
+        // F6 - behave like Alt-TAB
+        if (keypress.equals(kbF6)) {
+            application.switchWindow(true);
+            return;
+        }
+
+        // Shift-F6 - behave like Shift-Alt-TAB
+        if (keypress.equals(kbShiftF6)) {
+            application.switchWindow(false);
+            return;
+        }
+
+        // F5 - zoom
+        if (keypress.equals(kbF5)) {
+            if (maximized) {
+                restore();
+            } else {
+                maximize();
+            }
+        }
+
+        // Ctrl-F5 - size/move
+        if (keypress.equals(kbCtrlF5)) {
+            inKeyboardResize = !inKeyboardResize;
+        }
+
+        // I didn't take it, pass it on to my children
+        super.onKeypress(keypress);
+    }
+
+    /**
+     * Handle posted command events.
+     *
+     * @param command command event
+     */
+    @Override
+    public void onCommand(final TCommandEvent command) {
+
+        // These commands will typically not be seen unless a subclass
+        // overrides onMenu() due to how TApplication dispatches
+        // accelerators.
+
+        if (command.equals(cmWindowClose)) {
+            application.closeWindow(this);
+            return;
+        }
+
+        if (command.equals(cmWindowNext)) {
+            application.switchWindow(true);
+            return;
+        }
+
+        if (command.equals(cmWindowPrevious)) {
+            application.switchWindow(false);
+            return;
+        }
+
+        if (command.equals(cmWindowMove)) {
+            inKeyboardResize = true;
+            return;
+        }
+
+        if (command.equals(cmWindowZoom)) {
+            if (maximized) {
+                restore();
+            } else {
+                maximize();
+            }
+        }
+
+        // I didn't take it, pass it on to my children
+        super.onCommand(command);
+    }
+
+    /**
+     * Handle posted menu events.
+     *
+     * @param menu menu event
+     */
+    @Override
+    public void onMenu(final TMenuEvent menu) {
+        if (menu.getId() == TMenu.MID_WINDOW_CLOSE) {
+            application.closeWindow(this);
+            return;
+        }
+
+        if (menu.getId() == TMenu.MID_WINDOW_NEXT) {
+            application.switchWindow(true);
+            return;
+        }
+
+        if (menu.getId() == TMenu.MID_WINDOW_PREVIOUS) {
+            application.switchWindow(false);
+            return;
+        }
+
+        if (menu.getId() == TMenu.MID_WINDOW_MOVE) {
+            inKeyboardResize = true;
+            return;
+        }
+
+        if (menu.getId() == TMenu.MID_WINDOW_ZOOM) {
+            if (maximized) {
+                restore();
+            } else {
+                maximize();
+            }
+            return;
+        }
+
+        // I didn't take it, pass it on to my children
+        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.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param attr attributes to use (bold, foreColor, backColor)
+     */
+    public final void putAttrXY(final int x, final int y,
+        final CellAttributes attr) {
+
+        getScreen().putAttrXY(x, y, attr);
+    }
+
+    /**
+     * Set the attributes at one location.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param attr attributes to use (bold, foreColor, backColor)
+     * @param clip if true, honor clipping/offset
+     */
+    public final void putAttrXY(final int x, final int y,
+        final CellAttributes attr, final boolean clip) {
+
+        getScreen().putAttrXY(x, y, attr, clip);
+    }
+
+    /**
+     * Fill the entire screen with one character with attributes.
+     *
+     * @param ch character to draw
+     * @param attr attributes to use (bold, foreColor, backColor)
+     */
+    public final void putAll(final char ch, final CellAttributes attr) {
+        getScreen().putAll(ch, attr);
+    }
+
+    /**
+     * Render one character with attributes.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param ch character + attributes to draw
+     */
+    public final void putCharXY(final int x, final int y, final Cell ch) {
+        getScreen().putCharXY(x, y, ch);
+    }
+
+    /**
+     * Render one character with attributes.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param ch character to draw
+     * @param attr attributes to use (bold, foreColor, backColor)
+     */
+    public final void putCharXY(final int x, final int y, final char ch,
+        final CellAttributes attr) {
+
+        getScreen().putCharXY(x, y, ch, attr);
+    }
+
+    /**
+     * Render one character without changing the underlying attributes.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param ch character to draw
+     */
+    public final void putCharXY(final int x, final int y, final char ch) {
+        getScreen().putCharXY(x, y, ch);
+    }
+
+    /**
+     * Render a string.  Does not wrap if the string exceeds the line.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param str string to draw
+     * @param attr attributes to use (bold, foreColor, backColor)
+     */
+    public final void putStrXY(final int x, final int y, final String str,
+        final CellAttributes attr) {
+
+        getScreen().putStrXY(x, y, str, attr);
+    }
+
+    /**
+     * Render a string without changing the underlying attribute.  Does not
+     * wrap if the string exceeds the line.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param str string to draw
+     */
+    public final void putStrXY(final int x, final int y, final String str) {
+        getScreen().putStrXY(x, y, str);
+    }
+
+    /**
+     * Draw a vertical line from (x, y) to (x, y + n).
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param n number of characters to draw
+     * @param ch character to draw
+     * @param attr attributes to use (bold, foreColor, backColor)
+     */
+    public final void vLineXY(final int x, final int y, final int n,
+        final char ch, final CellAttributes attr) {
+
+        getScreen().vLineXY(x, y, n, ch, attr);
+    }
+
+    /**
+     * Draw a horizontal line from (x, y) to (x + n, y).
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param n number of characters to draw
+     * @param ch character to draw
+     * @param attr attributes to use (bold, foreColor, backColor)
+     */
+    public final void hLineXY(final int x, final int y, final int n,
+        final char ch, final CellAttributes attr) {
+
+        getScreen().hLineXY(x, y, n, ch, attr);
+    }
+
+
+}
index 747e757f24f9ce27c589414ac46ac3dc68ad6c4f..3f2ab92113a6a23783a028344e41cee1c6621147 100644 (file)
@@ -45,7 +45,7 @@ public final class GraphicsChars {
     /**
      * The CP437 to Unicode translation map.
      */
-    private static final char [] CP437 = {
+    public static final char [] CP437 = {
         '\u2007', '\u263A', '\u263B', '\u2665',
         '\u2666', '\u2663', '\u2660', '\u2022',
         '\u25D8', '\u25CB', '\u25D9', '\u2642',
index 0537bbedfaa2991894ba4d4f7a034d8f1e305dd3..f97e1e992b1dfa02f81472e34bf7fe4c407f03fd 100644 (file)
@@ -36,7 +36,7 @@ package jexer.bits;
  * characters, e.g. "&File && Stuff" would be "File & Stuff" with the first
  * 'F' highlighted.
  */
-public class MnemonicString {
+public final class MnemonicString {
 
     /**
      * Keyboard shortcut to activate this item.
@@ -48,11 +48,29 @@ public class MnemonicString {
      */
     private int shortcutIdx = -1;
 
+    /**
+     * Get location of the highlighted character.
+     *
+     * @return location of the highlighted character
+     */
+    public int getShortcutIdx() {
+        return shortcutIdx;
+    }
+
     /**
      * The raw (uncolored) string.
      */
     private String rawLabel;
 
+    /**
+     * Get the raw (uncolored) string.
+     *
+     * @return the raw (uncolored) string
+     */
+    public String getRawLabel() {
+        return rawLabel;
+    }
+    
     /**
      * Public constructor.
      *
index 29e0c1b42c51ccd775a348146679145cdc9dd615..8dcaed2778b462a59c407193b76532f0f90040f3 100644 (file)
@@ -31,7 +31,9 @@
 package jexer.event;
 
 /**
- * This class encapsulates several kinds of mouse input events.
+ * This class encapsulates several kinds of mouse input events.  Note that
+ * the relative (x,y) ARE MUTABLE: TWidget's onMouse() handlers perform that
+ * update during event dispatching.
  */
 public final class TMouseEvent extends TInputEvent {
 
@@ -84,6 +86,18 @@ public final class TMouseEvent extends TInputEvent {
         return x;
     }
 
+    /**
+     * Set x.
+     *
+     * @param x new relative X value
+     * @see jexer.TWidget#onMouseDown(TMouseEvent mouse)
+     * @see jexer.TWidget#onMouseDown(TMouseEvent mouse)
+     * @see jexer.TWidget#onMouseMotion(TMouseEvent mouse)
+     */
+    public void setX(final int x) {
+        this.x = x;
+    }
+
     /**
      * Mouse Y - relative coordinates.
      */
@@ -98,6 +112,18 @@ public final class TMouseEvent extends TInputEvent {
         return y;
     }
 
+    /**
+     * Set y.
+     *
+     * @param y new relative Y value
+     * @see jexer.TWidget#onMouseDown(TMouseEvent mouse)
+     * @see jexer.TWidget#onMouseDown(TMouseEvent mouse)
+     * @see jexer.TWidget#onMouseMotion(TMouseEvent mouse)
+     */
+    public void setY(final int y) {
+        this.y = y;
+    }
+
     /**
      * Mouse X - absolute screen coordinates.
      */
index 630725411a55210139995eb29b72a0a68f99a66a..6c872b3107ed86a5050d1951d6026806f6ee7771 100644 (file)
@@ -53,32 +53,122 @@ public abstract class Screen {
     /**
      * Drawing offset for x.
      */
-    public int offsetX;
+    protected int offsetX;
+
+    /**
+     * Set drawing offset for x.
+     *
+     * @param offsetX new drawing offset
+     */
+    public final void setOffsetX(final int offsetX) {
+        this.offsetX = offsetX;
+    }
 
     /**
      * Drawing offset for y.
      */
-    public int offsetY;
+    protected int offsetY;
 
+    /**
+     * Set drawing offset for y.
+     *
+     * @param offsetY new drawing offset
+     */
+    public final void setOffsetY(final int offsetY) {
+        this.offsetY = offsetY;
+    }
+    
     /**
      * Ignore anything drawn right of clipRight.
      */
-    public int clipRight;
+    protected int clipRight;
+
+    /**
+     * Get right drawing clipping boundary.
+     *
+     * @return drawing boundary
+     */
+    public final int getClipRight() {
+        return clipRight;
+    }
+
+    /**
+     * Set right drawing clipping boundary.
+     *
+     * @param clipRight new boundary
+     */
+    public final void setClipRight(final int clipRight) {
+        this.clipRight = clipRight;
+    }
 
     /**
      * Ignore anything drawn below clipBottom.
      */
-    public int clipBottom;
+    protected int clipBottom;
+
+    /**
+     * Get bottom drawing clipping boundary.
+     *
+     * @return drawing boundary
+     */
+    public final int getClipBottom() {
+        return clipBottom;
+    }
+
+    /**
+     * Set bottom drawing clipping boundary.
+     *
+     * @param clipBottom new boundary
+     */
+    public final void setClipBottom(final int clipBottom) {
+        this.clipBottom = clipBottom;
+    }
 
     /**
      * Ignore anything drawn left of clipLeft.
      */
-    public int clipLeft;
+    protected int clipLeft;
+
+    /**
+     * Get left drawing clipping boundary.
+     *
+     * @return drawing boundary
+     */
+    public final int getClipLeft() {
+        return clipLeft;
+    }
+
+    /**
+     * Set left drawing clipping boundary.
+     *
+     * @param clipLeft new boundary
+     */
+    public final void setClipLeft(final int clipLeft) {
+        this.clipLeft = clipLeft;
+    }
 
     /**
      * Ignore anything drawn above clipTop.
      */
-    public int clipTop;
+    protected int clipTop;
+
+    /**
+     * Get top drawing clipping boundary.
+     *
+     * @return drawing boundary
+     */
+    public final int getClipTop() {
+        return clipTop;
+    }
+
+    /**
+     * Set top drawing clipping boundary.
+     *
+     * @param clipTop new boundary
+     */
+    public final void setClipTop(final int clipTop) {
+        this.clipTop = clipTop;
+    }
 
     /**
      * The physical screen last sent out on flush().
@@ -93,7 +183,7 @@ public abstract class Screen {
     /**
      * When true, logical != physical.
      */
-    public boolean dirty;
+    protected boolean dirty;
 
     /**
      * Set if the user explicitly wants to redraw everything starting with a