Merge commit '8a1cae77d279cc246a68109f6178b2bb05e7471f'
[fanfix.git] / src / jexer / TWidget.java
index e8cd1a0e7e68a79f83d6de41f685a39aad379b81..d60efd8d3a321236e2069199c6db2fb364505d69 100644 (file)
@@ -28,6 +28,7 @@
  */
 package jexer;
 
+import java.awt.image.BufferedImage;
 import java.io.IOException;
 import java.util.List;
 import java.util.ArrayList;
@@ -35,6 +36,7 @@ import java.util.ArrayList;
 import jexer.backend.Screen;
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
 import jexer.bits.ColorTheme;
 import jexer.event.TCommandEvent;
 import jexer.event.TInputEvent;
@@ -42,6 +44,7 @@ import jexer.event.TKeypressEvent;
 import jexer.event.TMenuEvent;
 import jexer.event.TMouseEvent;
 import jexer.event.TResizeEvent;
+import jexer.layout.LayoutManager;
 import jexer.menu.TMenu;
 import jexer.ttree.TTreeItem;
 import jexer.ttree.TTreeView;
@@ -135,6 +138,11 @@ public abstract class TWidget implements Comparable<TWidget> {
      */
     private int cursorY = 0;
 
+    /**
+     * Layout manager.
+     */
+    private LayoutManager layout = null;
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -177,17 +185,7 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @param enabled if true assume enabled
      */
     protected TWidget(final TWidget parent, final boolean enabled) {
-        this.enabled = enabled;
-        this.parent = parent;
-        this.window = parent.window;
-        children = new ArrayList<TWidget>();
-
-        // Do not add TStatusBars, they are drawn by TApplication.
-        if (this instanceof TStatusBar) {
-            // NOP
-        } else {
-            parent.addChild(this);
-        }
+        this(parent, enabled, 0, 0, 0, 0);
     }
 
     /**
@@ -203,22 +201,26 @@ public abstract class TWidget implements Comparable<TWidget> {
     protected TWidget(final TWidget parent, final boolean enabled,
         final int x, final int y, final int width, final int height) {
 
+        if (width < 0) {
+            throw new IllegalArgumentException("width cannot be negative");
+        }
+        if (height < 0) {
+            throw new IllegalArgumentException("height cannot be negative");
+        }
+
         this.enabled = enabled;
         this.parent = parent;
-        this.window = parent.window;
         children = new ArrayList<TWidget>();
 
-        // Do not add TStatusBars, they are drawn by TApplication.
-        if (this instanceof TStatusBar) {
-            // NOP
-        } else {
-            parent.addChild(this);
-        }
-
         this.x = x;
         this.y = y;
         this.width = width;
         this.height = height;
+
+        if (parent != null) {
+            this.window = parent.window;
+            parent.addChild(this);
+        }
     }
 
     /**
@@ -233,6 +235,13 @@ public abstract class TWidget implements Comparable<TWidget> {
     protected final void setupForTWindow(final TWindow window,
         final int x, final int y, final int width, final int height) {
 
+        if (width < 0) {
+            throw new IllegalArgumentException("width cannot be negative");
+        }
+        if (height < 0) {
+            throw new IllegalArgumentException("height cannot be negative");
+        }
+
         this.parent = window;
         this.window = window;
         this.x      = x;
@@ -291,6 +300,7 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @param keypress keystroke event
      */
     public void onKeypress(final TKeypressEvent keypress) {
+        assert (parent != null);
 
         if ((children.size() == 0)
             || (this instanceof TTreeView)
@@ -331,7 +341,7 @@ public abstract class TWidget implements Comparable<TWidget> {
         }
 
         // If I have any buttons on me AND this is an Alt-key that matches
-        // its mnemonic, send it an Enter keystroke
+        // its mnemonic, send it an Enter keystroke.
         for (TWidget widget: children) {
             if (widget instanceof TButton) {
                 TButton button = (TButton) widget;
@@ -349,6 +359,81 @@ public abstract class TWidget implements Comparable<TWidget> {
             }
         }
 
+        // If I have any labels on me AND this is an Alt-key that matches
+        // its mnemonic, call its action.
+        for (TWidget widget: children) {
+            if (widget instanceof TLabel) {
+                TLabel label = (TLabel) widget;
+                if (!keypress.getKey().isFnKey()
+                    && keypress.getKey().isAlt()
+                    && !keypress.getKey().isCtrl()
+                    && (Character.toLowerCase(label.getMnemonic().getShortcut())
+                        == Character.toLowerCase(keypress.getKey().getChar()))
+                ) {
+
+                    label.dispatch();
+                    return;
+                }
+            }
+        }
+
+        // If I have any radiobuttons on me AND this is an Alt-key that
+        // matches its mnemonic, select it and send a Space to it.
+        for (TWidget widget: children) {
+            if (widget instanceof TRadioButton) {
+                TRadioButton button = (TRadioButton) widget;
+                if (button.isEnabled()
+                    && !keypress.getKey().isFnKey()
+                    && keypress.getKey().isAlt()
+                    && !keypress.getKey().isCtrl()
+                    && (Character.toLowerCase(button.getMnemonic().getShortcut())
+                        == Character.toLowerCase(keypress.getKey().getChar()))
+                ) {
+                    activate(widget);
+                    widget.onKeypress(new TKeypressEvent(kbSpace));
+                    return;
+                }
+            }
+            if (widget instanceof TRadioGroup) {
+                for (TWidget child: widget.getChildren()) {
+                    if (child instanceof TRadioButton) {
+                        TRadioButton button = (TRadioButton) child;
+                        if (button.isEnabled()
+                            && !keypress.getKey().isFnKey()
+                            && keypress.getKey().isAlt()
+                            && !keypress.getKey().isCtrl()
+                            && (Character.toLowerCase(button.getMnemonic().getShortcut())
+                                == Character.toLowerCase(keypress.getKey().getChar()))
+                        ) {
+                            activate(widget);
+                            widget.activate(child);
+                            child.onKeypress(new TKeypressEvent(kbSpace));
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+
+        // If I have any checkboxes on me AND this is an Alt-key that matches
+        // its mnemonic, select it and set it to checked.
+        for (TWidget widget: children) {
+            if (widget instanceof TCheckBox) {
+                TCheckBox checkBox = (TCheckBox) widget;
+                if (checkBox.isEnabled()
+                    && !keypress.getKey().isFnKey()
+                    && keypress.getKey().isAlt()
+                    && !keypress.getKey().isCtrl()
+                    && (Character.toLowerCase(checkBox.getMnemonic().getShortcut())
+                        == Character.toLowerCase(keypress.getKey().getChar()))
+                ) {
+                    activate(checkBox);
+                    checkBox.setChecked(true);
+                    return;
+                }
+            }
+        }
+
         // Dispatch the keypress to an active widget
         for (TWidget widget: children) {
             if (widget.active) {
@@ -485,6 +570,14 @@ public abstract class TWidget implements Comparable<TWidget> {
         if (resize.getType() == TResizeEvent.Type.WIDGET) {
             width = resize.getWidth();
             height = resize.getHeight();
+            if (layout != null) {
+                if (this instanceof TWindow) {
+                    layout.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+                            width - 2, height - 2));
+                } else {
+                    layout.onResize(resize);
+                }
+            }
         } else {
             // Let children see the screen resize
             for (TWidget widget: children) {
@@ -499,9 +592,8 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @param command command event
      */
     public void onCommand(final TCommandEvent command) {
-        // Default: do nothing, pass to children instead
-        for (TWidget widget: children) {
-            widget.onCommand(command);
+        if (activeChild != null) {
+            activeChild.onCommand(command);
         }
     }
 
@@ -610,6 +702,112 @@ public abstract class TWidget implements Comparable<TWidget> {
         return children;
     }
 
+    /**
+     * Remove this widget from its parent container.  close() will be called
+     * before it is removed.
+     */
+    public final void remove() {
+        remove(true);
+    }
+
+    /**
+     * Remove this widget from its parent container.
+     *
+     * @param doClose if true, call the close() method before removing the
+     * child
+     */
+    public final void remove(final boolean doClose) {
+        if (parent != null) {
+            parent.remove(this, doClose);
+        }
+    }
+
+    /**
+     * Remove a child widget from this container.
+     *
+     * @param child the child widget to remove
+     */
+    public final void remove(final TWidget child) {
+        remove(child, true);
+    }
+
+    /**
+     * Remove a child widget from this container.
+     *
+     * @param child the child widget to remove
+     * @param doClose if true, call the close() method before removing the
+     * child
+     */
+    public final void remove(final TWidget child, final boolean doClose) {
+        if (!children.contains(child)) {
+            throw new IndexOutOfBoundsException("child widget is not in " +
+                "list of children of this parent");
+        }
+        if (doClose) {
+            child.close();
+        }
+        children.remove(child);
+        child.parent = null;
+        child.window = null;
+        if (layout != null) {
+            layout.remove(this);
+        }
+    }
+
+    /**
+     * Set this widget's parent to a different widget.
+     *
+     * @param newParent new parent widget
+     * @param doClose if true, call the close() method before removing the
+     * child from its existing parent widget
+     */
+    public final void setParent(final TWidget newParent,
+        final boolean doClose) {
+
+        if (parent != null) {
+            parent.remove(this, doClose);
+            window = null;
+        }
+        assert (parent == null);
+        assert (window == null);
+        parent = newParent;
+        setWindow(parent.window);
+        parent.addChild(this);
+    }
+
+    /**
+     * Set this widget's window to a specific window.
+     *
+     * Having a null parent with a specified window is only used within Jexer
+     * by TStatusBar because TApplication routes events directly to it and
+     * calls its draw() method.  Any other non-parented widgets will require
+     * similar special case functionality to receive events or be drawn to
+     * screen.
+     *
+     * @param window the window to use
+     */
+    public final void setWindow(final TWindow window) {
+        this.window = window;
+        for (TWidget child: getChildren()) {
+            child.setWindow(window);
+        }
+    }
+
+    /**
+     * Remove a child widget from this container, and all of its children
+     * recursively from their parent containers.
+     *
+     * @param child the child widget to remove
+     * @param doClose if true, call the close() method before removing each
+     * child
+     */
+    public final void removeAll(final TWidget child, final boolean doClose) {
+        remove(child, doClose);
+        for (TWidget w: child.children) {
+            child.removeAll(w, doClose);
+        }
+    }
+
     /**
      * Get active flag.
      *
@@ -678,7 +876,7 @@ public abstract class TWidget implements Comparable<TWidget> {
      *
      * @return widget width
      */
-    public final int getWidth() {
+    public int getWidth() {
         return this.width;
     }
 
@@ -687,8 +885,12 @@ public abstract class TWidget implements Comparable<TWidget> {
      *
      * @param width new widget width
      */
-    public final void setWidth(final int width) {
+    public void setWidth(final int width) {
         this.width = width;
+        if (layout != null) {
+            layout.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+                    width, height));
+        }
     }
 
     /**
@@ -696,7 +898,7 @@ public abstract class TWidget implements Comparable<TWidget> {
      *
      * @return widget height
      */
-    public final int getHeight() {
+    public int getHeight() {
         return this.height;
     }
 
@@ -705,8 +907,12 @@ public abstract class TWidget implements Comparable<TWidget> {
      *
      * @param height new widget height
      */
-    public final void setHeight(final int height) {
+    public void setHeight(final int height) {
         this.height = height;
+        if (layout != null) {
+            layout.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+                    width, height));
+        }
     }
 
     /**
@@ -720,10 +926,44 @@ public abstract class TWidget implements Comparable<TWidget> {
     public final void setDimensions(final int x, final int y, final int width,
         final int height) {
 
-        setX(x);
-        setY(y);
+        this.x = x;
+        this.y = y;
+        // Call the functions so that subclasses can choose how to handle it.
         setWidth(width);
         setHeight(height);
+        if (layout != null) {
+            layout.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+                    width, height));
+        }
+    }
+
+    /**
+     * Get the layout manager.
+     *
+     * @return the layout manager, or null if not set
+     */
+    public LayoutManager getLayoutManager() {
+        return layout;
+    }
+
+    /**
+     * Set the layout manager.
+     *
+     * @param layout the new layout manager
+     */
+    public void setLayoutManager(LayoutManager layout) {
+        if (this.layout != null) {
+            for (TWidget w: children) {
+                this.layout.remove(w);
+            }
+            this.layout = null;
+        }
+        this.layout = layout;
+        if (this.layout != null) {
+            for (TWidget w: children) {
+                this.layout.add(w);
+            }
+        }
     }
 
     /**
@@ -806,6 +1046,13 @@ public abstract class TWidget implements Comparable<TWidget> {
             return false;
         }
 
+        assert (window != null);
+
+        if (window instanceof TDesktop) {
+            // Desktop doesn't have a window border.
+            return cursorVisible;
+        }
+
         // If cursor is out of my window's bounds, it is not visible.
         if ((getCursorAbsoluteX() >= window.getAbsoluteX()
                 + window.getWidth() - 1)
@@ -858,19 +1105,37 @@ public abstract class TWidget implements Comparable<TWidget> {
     /**
      * Get this TWidget's parent TApplication.
      *
-     * @return the parent TApplication
+     * @return the parent TApplication, or null if not assigned
      */
     public TApplication getApplication() {
-        return window.getApplication();
+        if (window != null) {
+            return window.getApplication();
+        }
+        return null;
     }
 
     /**
      * Get the Screen.
      *
-     * @return the Screen
+     * @return the Screen, or null if not assigned
      */
     public Screen getScreen() {
-        return window.getScreen();
+        if (window != null) {
+            return window.getScreen();
+        }
+        return null;
+    }
+
+    /**
+     * Get the Clipboard.
+     *
+     * @return the Clipboard, or null if not assigned
+     */
+    public Clipboard getClipboard() {
+        if (window != null) {
+            return window.getApplication().getClipboard();
+        }
+        return null;
     }
 
     /**
@@ -885,7 +1150,8 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @return difference between this.tabOrder and that.tabOrder, or
      * difference between this.z and that.z, or String.compareTo(text)
      */
-    public final int compareTo(final TWidget that) {
+    @Override
+    public int compareTo(final TWidget that) {
         if ((this instanceof TWindow)
             && (that instanceof TWindow)
         ) {
@@ -910,7 +1176,7 @@ public abstract class TWidget implements Comparable<TWidget> {
         if (parent == this) {
             return active;
         }
-        return (active && parent.isAbsoluteActive());
+        return (active && (parent == null ? true : parent.isAbsoluteActive()));
     }
 
     /**
@@ -984,6 +1250,25 @@ public abstract class TWidget implements Comparable<TWidget> {
         return window.getApplication().getTheme();
     }
 
+    /**
+     * See if this widget can be drawn onto a screen.
+     *
+     * @return true if this widget is part of the hierarchy that can draw to
+     * a screen
+     */
+    public final boolean isDrawable() {
+        if ((window == null)
+            || (window.getScreen() == null)
+            || (parent == null)
+        ) {
+            return false;
+        }
+        if (parent == this) {
+            return true;
+        }
+        return (parent.isDrawable());
+    }
+
     /**
      * Draw my specific widget.  When called, the screen rectangle I draw
      * into is already setup (offset and clipping).
@@ -996,6 +1281,10 @@ public abstract class TWidget implements Comparable<TWidget> {
      * Called by parent to render to TWindow.  Note package private access.
      */
     final void drawChildren() {
+        if (!isDrawable()) {
+            return;
+        }
+
         // Set my clipping rectangle
         assert (window != null);
         assert (getScreen() != null);
@@ -1012,10 +1301,16 @@ public abstract class TWidget implements Comparable<TWidget> {
 
         int absoluteRightEdge = window.getAbsoluteX() + window.getWidth();
         int absoluteBottomEdge = window.getAbsoluteY() + window.getHeight();
-        if (!(this instanceof TWindow) && !(this instanceof TVScroller)) {
+        if (!(this instanceof TWindow)
+            && !(this instanceof TVScroller)
+            && !(window instanceof TDesktop)
+        ) {
             absoluteRightEdge -= 1;
         }
-        if (!(this instanceof TWindow) && !(this instanceof THScroller)) {
+        if (!(this instanceof TWindow)
+            && !(this instanceof THScroller)
+            && !(window instanceof TDesktop)
+        ) {
             absoluteBottomEdge -= 1;
         }
         int myRightEdge = getAbsoluteX() + width;
@@ -1041,12 +1336,24 @@ public abstract class TWidget implements Comparable<TWidget> {
 
         // Draw me
         draw();
+        if (!isDrawable()) {
+            // An action taken by a draw method unhooked me from the UI.
+            // Bail out.
+            return;
+        }
+
+        assert (visible == true);
 
         // Continue down the chain.  Draw the active child last so that it
         // is on top.
         for (TWidget widget: children) {
             if (widget.isVisible() && (widget != activeChild)) {
                 widget.drawChildren();
+                if (!isDrawable()) {
+                    // An action taken by a draw method unhooked me from the UI.
+                    // Bail out.
+                    return;
+                }
             }
         }
         if (activeChild != null) {
@@ -1067,7 +1374,7 @@ public abstract class TWidget implements Comparable<TWidget> {
      *
      * @param child TWidget to add
      */
-    private void addChild(final TWidget child) {
+    public void addChild(final TWidget child) {
         children.add(child);
 
         if ((child.enabled)
@@ -1083,6 +1390,42 @@ public abstract class TWidget implements Comparable<TWidget> {
         for (int i = 0; i < children.size(); i++) {
             children.get(i).tabOrder = i;
         }
+        if (layout != null) {
+            layout.add(child);
+        }
+    }
+
+    /**
+     * Reset the tab order of children to match their position in the list.
+     * Available so that subclasses can re-order their widgets if needed.
+     */
+    protected void resetTabOrder() {
+        for (int i = 0; i < children.size(); i++) {
+            children.get(i).tabOrder = i;
+        }
+    }
+    
+    /**
+     * Remove and {@link TWidget#close()} the given child from this {@link TWidget}.
+     * <p>
+     * Will also reorder the tab values of the remaining children.
+     * 
+     * @param child the child to remove
+     * 
+     * @return TRUE if the child was removed, FALSE if it was not found
+     */
+    public boolean removeChild(final TWidget child) {
+        if (children.remove(child)) {
+                child.close();
+                child.parent = null;
+                child.window = null;
+                
+                resetTabOrder();
+                
+                return true;
+        }
+        
+        return false;
     }
 
     /**
@@ -1108,9 +1451,9 @@ public abstract class TWidget implements Comparable<TWidget> {
                 if (activeChild != null) {
                     activeChild.active = false;
                 }
-                child.active = true;
-                activeChild = child;
             }
+            child.active = true;
+            activeChild = child;
         }
     }
 
@@ -1129,9 +1472,6 @@ public abstract class TWidget implements Comparable<TWidget> {
             return;
         }
 
-        if (activeChild == null) {
-            return;
-        }
         TWidget child = null;
         for (TWidget widget: children) {
             if ((widget.enabled)
@@ -1144,13 +1484,40 @@ public abstract class TWidget implements Comparable<TWidget> {
             }
         }
         if ((child != null) && (child != activeChild)) {
-            activeChild.active = false;
+            if (activeChild != null) {
+                activeChild.active = false;
+            }
             assert (child.enabled);
             child.active = true;
             activeChild = child;
         }
     }
 
+    /**
+     * Make this widget the active child of its parent.  Note that this is
+     * not final since TWindow overrides activate().
+     */
+    public void activate() {
+        if (enabled) {
+            if (parent != null) {
+                parent.activate(this);
+            }
+        }
+    }
+
+    /**
+     * Make this widget, all of its parents, the active child.
+     */
+    public final void activateAll() {
+        activate();
+        if (parent == this) {
+            return;
+        }
+        if (parent != null) {
+            parent.activateAll();
+        }
+    }
+
     /**
      * Switch the active widget with the next in the tab order.
      *
@@ -1164,6 +1531,8 @@ public abstract class TWidget implements Comparable<TWidget> {
             return;
         }
 
+        assert (parent != null);
+
         // If there is only one child, make it active if it is enabled.
         if (children.size() == 1) {
             if (children.get(0).enabled == true) {
@@ -1178,7 +1547,10 @@ public abstract class TWidget implements Comparable<TWidget> {
 
         // Two or more children: go forward or backward to the next enabled
         // child.
-        int tabOrder = activeChild.tabOrder;
+        int tabOrder = 0;
+        if (activeChild != null) {
+            tabOrder = activeChild.tabOrder;
+        }
         do {
             if (forward) {
                 tabOrder++;
@@ -1203,7 +1575,12 @@ public abstract class TWidget implements Comparable<TWidget> {
 
                 tabOrder = 0;
             }
-            if (activeChild.tabOrder == tabOrder) {
+            if (activeChild == null) {
+                if (tabOrder == 0) {
+                    // We wrapped around
+                    break;
+                }
+            } else if (activeChild.tabOrder == tabOrder) {
                 // We wrapped around
                 break;
             }
@@ -1211,11 +1588,15 @@ public abstract class TWidget implements Comparable<TWidget> {
             && !(children.get(tabOrder) instanceof THScroller)
             && !(children.get(tabOrder) instanceof TVScroller));
 
-        assert (children.get(tabOrder).enabled);
+        if (activeChild != null) {
+            assert (children.get(tabOrder).enabled);
 
-        activeChild.active = false;
-        children.get(tabOrder).active = true;
-        activeChild = children.get(tabOrder);
+            activeChild.active = false;
+        }
+        if (children.get(tabOrder).enabled == true) {
+            children.get(tabOrder).active = true;
+            activeChild = children.get(tabOrder);
+        }
     }
 
     /**
@@ -1239,6 +1620,153 @@ public abstract class TWidget implements Comparable<TWidget> {
         return this;
     }
 
+    /**
+     * Insert a vertical split between this widget and parent, and optionally
+     * put another widget in the other side of the split.
+     *
+     * @param newWidgetOnLeft if true, the new widget (if specified) will be
+     * on the left pane, and this widget will be placed on the right pane
+     * @param newWidget the new widget to add to the other pane, or null
+     * @return the new split pane widget
+     */
+    public TSplitPane splitVertical(final boolean newWidgetOnLeft,
+        final TWidget newWidget) {
+
+        TSplitPane splitPane = new TSplitPane(null, x, y, width, height, true);
+        TWidget myParent = parent;
+        remove(false);
+        if (myParent instanceof TSplitPane) {
+            // TSplitPane has a left/right/top/bottom link to me somewhere,
+            // replace it with a link to splitPane.
+            ((TSplitPane) myParent).replaceWidget(this, splitPane);
+        }
+        splitPane.setParent(myParent, false);
+        if (newWidgetOnLeft) {
+            splitPane.setLeft(newWidget);
+            splitPane.setRight(this);
+        } else {
+            splitPane.setLeft(this);
+            splitPane.setRight(newWidget);
+        }
+        if (newWidget != null) {
+            newWidget.activateAll();
+        } else {
+            activateAll();
+        }
+
+        assert (parent != null);
+        assert (window != null);
+        assert (splitPane.getWindow() != null);
+        assert (splitPane.getParent() != null);
+        assert (splitPane.isActive() == true);
+        assert (parent == splitPane);
+        if (newWidget != null) {
+            assert (newWidget.parent == parent);
+            assert (newWidget.active == true);
+            assert (active == false);
+        } else {
+            assert (active == true);
+        }
+        return splitPane;
+    }
+
+    /**
+     * Insert a horizontal split between this widget and parent, and
+     * optionally put another widget in the other side of the split.
+     *
+     * @param newWidgetOnTop if true, the new widget (if specified) will be
+     * on the top pane, and this widget's children will be placed on the
+     * bottom pane
+     * @param newWidget the new widget to add to the other pane, or null
+     * @return the new split pane widget
+     */
+    public TSplitPane splitHorizontal(final boolean newWidgetOnTop,
+        final TWidget newWidget) {
+
+        TSplitPane splitPane = new TSplitPane(null, x, y, width, height, false);
+        TWidget myParent = parent;
+        remove(false);
+        if (myParent instanceof TSplitPane) {
+            // TSplitPane has a left/right/top/bottom link to me somewhere,
+            // replace it with a link to splitPane.
+            ((TSplitPane) myParent).replaceWidget(this, splitPane);
+        }
+        splitPane.setParent(myParent, false);
+        if (newWidgetOnTop) {
+            splitPane.setTop(newWidget);
+            splitPane.setBottom(this);
+        } else {
+            splitPane.setTop(this);
+            splitPane.setBottom(newWidget);
+        }
+        if (newWidget != null) {
+            newWidget.activateAll();
+        } else {
+            activateAll();
+        }
+
+        assert (parent != null);
+        assert (window != null);
+        assert (splitPane.getWindow() != null);
+        assert (splitPane.getParent() != null);
+        assert (splitPane.isActive() == true);
+        assert (parent == splitPane);
+        if (newWidget != null) {
+            assert (newWidget.parent == parent);
+            assert (newWidget.active == true);
+            assert (active == false);
+        } else {
+            assert (active == true);
+        }
+        return splitPane;
+    }
+
+    /**
+     * Generate a human-readable string for this widget.
+     *
+     * @return a human-readable string
+     */
+    @Override
+    public String toString() {
+        return String.format("%s(%8x) position (%d, %d) geometry %dx%d " +
+            "active %s enabled %s visible %s", getClass().getName(),
+            hashCode(), x, y, width, height, active, enabled, visible);
+    }
+
+    /**
+     * Generate a string for this widget's hierarchy.
+     *
+     * @param prefix a prefix to use for this widget's place in the hierarchy
+     * @return a pretty-printable string of this hierarchy
+     */
+    protected String toPrettyString(final String prefix) {
+        StringBuilder sb = new StringBuilder(prefix);
+        sb.append(toString());
+        String newPrefix = "";
+        for (int i = 0; i < prefix.length(); i++) {
+            newPrefix += " ";
+        }
+        for (int i = 0; i < children.size(); i++) {
+            TWidget child= children.get(i);
+            sb.append("\n");
+            if (i == children.size() - 1) {
+                sb.append(child.toPrettyString(newPrefix + " \u2514\u2500"));
+            } else {
+                sb.append(child.toPrettyString(newPrefix + " \u251c\u2500"));
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Generate a string for this widget's hierarchy.
+     *
+     * @return a pretty-printable string of this hierarchy
+     */
+    public String toPrettyString() {
+        return toPrettyString("");
+    }
+
     // ------------------------------------------------------------------------
     // Passthru for Screen functions ------------------------------------------
     // ------------------------------------------------------------------------
@@ -1287,7 +1815,7 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @param ch character to draw
      * @param attr attributes to use (bold, foreColor, backColor)
      */
-    protected final void putAll(final char ch, final CellAttributes attr) {
+    protected final void putAll(final int ch, final CellAttributes attr) {
         getScreen().putAll(ch, attr);
     }
 
@@ -1310,7 +1838,7 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @param ch character to draw
      * @param attr attributes to use (bold, foreColor, backColor)
      */
-    protected final void putCharXY(final int x, final int y, final char ch,
+    protected final void putCharXY(final int x, final int y, final int ch,
         final CellAttributes attr) {
 
         getScreen().putCharXY(x, y, ch, attr);
@@ -1323,7 +1851,7 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @param y row coordinate.  0 is the top-most row.
      * @param ch character to draw
      */
-    protected final void putCharXY(final int x, final int y, final char ch) {
+    protected final void putCharXY(final int x, final int y, final int ch) {
         getScreen().putCharXY(x, y, ch);
     }
 
@@ -1363,7 +1891,7 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @param attr attributes to use (bold, foreColor, backColor)
      */
     protected final void vLineXY(final int x, final int y, final int n,
-        final char ch, final CellAttributes attr) {
+        final int ch, final CellAttributes attr) {
 
         getScreen().vLineXY(x, y, n, ch, attr);
     }
@@ -1378,7 +1906,7 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @param attr attributes to use (bold, foreColor, backColor)
      */
     protected final void hLineXY(final int x, final int y, final int n,
-        final char ch, final CellAttributes attr) {
+        final int ch, final CellAttributes attr) {
 
         getScreen().hLineXY(x, y, n, ch, attr);
     }
@@ -1453,6 +1981,21 @@ public abstract class TWidget implements Comparable<TWidget> {
         return addLabel(text, x, y, "tlabel");
     }
 
+    /**
+     * Convenience function to add a label to this container/window.
+     *
+     * @param text label
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param action to call when shortcut is pressed
+     * @return the new label
+     */
+    public final TLabel addLabel(final String text, final int x, final int y,
+        final TAction action) {
+
+        return addLabel(text, x, y, "tlabel", action);
+    }
+
     /**
      * Convenience function to add a label to this container/window.
      *
@@ -1469,6 +2012,23 @@ public abstract class TWidget implements Comparable<TWidget> {
         return new TLabel(this, text, x, y, colorKey);
     }
 
+    /**
+     * Convenience function to add a label to this container/window.
+     *
+     * @param text label
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param colorKey ColorTheme key color to use for foreground text.
+     * Default is "tlabel"
+     * @param action to call when shortcut is pressed
+     * @return the new label
+     */
+    public final TLabel addLabel(final String text, final int x, final int y,
+        final String colorKey, final TAction action) {
+
+        return new TLabel(this, text, x, y, colorKey, action);
+    }
+
     /**
      * Convenience function to add a label to this container/window.
      *
@@ -1486,6 +2046,26 @@ public abstract class TWidget implements Comparable<TWidget> {
         return new TLabel(this, text, x, y, colorKey, useWindowBackground);
     }
 
+    /**
+     * Convenience function to add a label to this container/window.
+     *
+     * @param text label
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param colorKey ColorTheme key color to use for foreground text.
+     * Default is "tlabel"
+     * @param useWindowBackground if true, use the window's background color
+     * @param action to call when shortcut is pressed
+     * @return the new label
+     */
+    public final TLabel addLabel(final String text, final int x, final int y,
+        final String colorKey, final boolean useWindowBackground,
+        final TAction action) {
+
+        return new TLabel(this, text, x, y, colorKey, useWindowBackground,
+            action);
+    }
+
     /**
      * Convenience function to add a button to this container/window.
      *
@@ -1525,18 +2105,18 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @param values the possible values for the box, shown in the drop-down
      * @param valuesIndex the initial index in values, or -1 for no default
      * value
-     * @param valuesHeight the height of the values drop-down when it is
-     * visible
+     * @param maxValuesHeight the maximum height of the values drop-down when
+     * it is visible
      * @param updateAction action to call when a new value is selected from
      * the list or enter is pressed in the edit field
      * @return the new combobox
      */
     public final TComboBox addComboBox(final int x, final int y,
         final int width, final List<String> values, final int valuesIndex,
-        final int valuesHeight, final TAction updateAction) {
+        final int maxValuesHeight, final TAction updateAction) {
 
         return new TComboBox(this, x, y, width, values, valuesIndex,
-            valuesHeight, updateAction);
+            maxValuesHeight, updateAction);
     }
 
     /**
@@ -1600,6 +2180,21 @@ public abstract class TWidget implements Comparable<TWidget> {
         return new TRadioGroup(this, x, y, label);
     }
 
+    /**
+     * Convenience function to add a radio button group to this
+     * container/window.
+     *
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of group
+     * @param label label to display on the group box
+     */
+    public final TRadioGroup addRadioGroup(final int x, final int y,
+        final int width, final String label) {
+
+        return new TRadioGroup(this, x, y, width, label);
+    }
+
     /**
      * Convenience function to add a text field to this container/window.
      *
@@ -1761,6 +2356,22 @@ public abstract class TWidget implements Comparable<TWidget> {
         return getApplication().inputBox(title, caption, text);
     }
 
+    /**
+     * Convenience function to spawn an input box.
+     *
+     * @param title window title, will be centered along the top border
+     * @param caption message to display.  Use embedded newlines to get a
+     * multi-line box.
+     * @param text initial text to seed the field with
+     * @param type one of the Type constants.  Default is Type.OK.
+     * @return the new input box
+     */
+    public final TInputBox inputBox(final String title, final String caption,
+        final String text, final TInputBox.Type type) {
+
+        return getApplication().inputBox(title, caption, text, type);
+    }
+
     /**
      * Convenience function to add a password text field to this
      * container/window.
@@ -2052,4 +2663,132 @@ public abstract class TWidget implements Comparable<TWidget> {
             moveAction);
     }
 
+    /**
+     * Convenience function to add a list to this container/window.
+     *
+     * @param strings list of strings to show.  This is allowed to be null
+     * and set later with setList() or by subclasses.
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of text area
+     * @param height height of text area
+     * @param enterAction action to perform when an item is selected
+     * @param moveAction action to perform when the user navigates to a new
+     * item with arrow/page keys
+     * @param singleClickAction action to perform when the user clicks on an
+     * item
+     */
+    public TList addList(final List<String> strings, final int x,
+        final int y, final int width, final int height,
+        final TAction enterAction, final TAction moveAction,
+        final TAction singleClickAction) {
+
+        return new TList(this, strings, x, y, width, height, enterAction,
+            moveAction, singleClickAction);
+    }
+
+
+    /**
+     * Convenience function to add an image to this container/window.
+     *
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width number of text cells for width of the image
+     * @param height number of text cells for height of the image
+     * @param image the image to display
+     * @param left left column of the image.  0 is the left-most column.
+     * @param top top row of the image.  0 is the top-most row.
+     */
+    public final TImage addImage(final int x, final int y,
+        final int width, final int height,
+        final BufferedImage image, final int left, final int top) {
+
+        return new TImage(this, x, y, width, height, image, left, top);
+    }
+
+    /**
+     * Convenience function to add an image to this container/window.
+     *
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width number of text cells for width of the image
+     * @param height number of text cells for height of the image
+     * @param image the image to display
+     * @param left left column of the image.  0 is the left-most column.
+     * @param top top row of the image.  0 is the top-most row.
+     * @param clickAction function to call when mouse is pressed
+     */
+    public final TImage addImage(final int x, final int y,
+        final int width, final int height,
+        final BufferedImage image, final int left, final int top,
+        final TAction clickAction) {
+
+        return new TImage(this, x, y, width, height, image, left, top,
+            clickAction);
+    }
+
+    /**
+     * Convenience function to add an editable 2D data table to this
+     * container/window.
+     *
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of widget
+     * @param height height of widget
+     */
+    public TTableWidget addTable(final int x, final int y, final int width,
+        final int height) {
+
+        return new TTableWidget(this, x, y, width, height);
+    }
+
+    /**
+     * Convenience function to add an editable 2D data table to this
+     * container/window.
+     *
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of widget
+     * @param height height of widget
+     * @param gridColumns number of columns in grid
+     * @param gridRows number of rows in grid
+     */
+    public TTableWidget addTable(final int x, final int y, final int width,
+        final int height, final int gridColumns, final int gridRows) {
+
+        return new TTableWidget(this, x, y, width, height, gridColumns,
+            gridRows);
+    }
+
+    /**
+     * Convenience function to add a panel to this container/window.
+     *
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of text area
+     * @param height height of text area
+     * @return the new panel
+     */
+    public final TPanel addPanel(final int x, final int y, final int width,
+        final int height) {
+
+        return new TPanel(this, x, y, width, height);
+    }
+
+    /**
+     * Convenience function to add a split pane to this container/window.
+     *
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of text area
+     * @param height height of text area
+     * @param vertical if true, split vertically
+     * @return the new split pane
+     */
+    public final TSplitPane addSplitPane(final int x, final int y,
+        final int width, final int height, final boolean vertical) {
+
+        return new TSplitPane(this, x, y, width, height, vertical);
+    }
+
 }