TEditor 50% complete
[nikiroo-utils.git] / src / jexer / TApplication.java
index 9f15ccbc4f5d33b6b21291a151da2cd41c78a870..8b436ab9a99ec9219b9755ffce379c62d834e10f 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2016 Kevin Lamonte
+ * Copyright (C) 2017 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -44,7 +44,6 @@ import java.util.Map;
 
 import jexer.bits.CellAttributes;
 import jexer.bits.ColorTheme;
-import jexer.bits.GraphicsChars;
 import jexer.event.TCommandEvent;
 import jexer.event.TInputEvent;
 import jexer.event.TKeypressEvent;
@@ -52,16 +51,19 @@ import jexer.event.TMenuEvent;
 import jexer.event.TMouseEvent;
 import jexer.event.TResizeEvent;
 import jexer.backend.Backend;
+import jexer.backend.Screen;
 import jexer.backend.SwingBackend;
 import jexer.backend.ECMA48Backend;
-import jexer.io.Screen;
+import jexer.backend.TWindowBackend;
 import jexer.menu.TMenu;
 import jexer.menu.TMenuItem;
 import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
 /**
- * TApplication sets up a full Text User Interface application.
+ * TApplication is the main driver class for a full Text User Interface
+ * application.  It manages windows, provides a menu bar and status bar, and
+ * processes events received from the user.
  */
 public class TApplication implements Runnable {
 
@@ -404,7 +406,15 @@ public class TApplication implements Runnable {
      * @return the Screen
      */
     public final Screen getScreen() {
-        return backend.getScreen();
+        if (backend instanceof TWindowBackend) {
+            // We are being rendered to a TWindow.  We can't use its
+            // getScreen() method because that is how it is rendering to a
+            // hardware backend somewhere.  Instead use its getOtherScreen()
+            // method.
+            return ((TWindowBackend) backend).getOtherScreen();
+        } else {
+            return backend.getScreen();
+        }
     }
 
     /**
@@ -449,7 +459,7 @@ public class TApplication implements Runnable {
     private List<TMenu> subMenus;
 
     /**
-     * The currently acive menu.
+     * The currently active menu.
      */
     private TMenu activeMenu = null;
 
@@ -482,6 +492,11 @@ public class TApplication implements Runnable {
      */
     private List<TWindow> windows;
 
+    /**
+     * The currently acive window.
+     */
+    private TWindow activeWindow = null;
+
     /**
      * Timers that are being ticked.
      */
@@ -527,6 +542,80 @@ public class TApplication implements Runnable {
         return desktopBottom;
     }
 
+    /**
+     * An optional TDesktop background window that is drawn underneath
+     * everything else.
+     */
+    private TDesktop desktop;
+
+    /**
+     * Set the TDesktop instance.
+     *
+     * @param desktop a TDesktop instance, or null to remove the one that is
+     * set
+     */
+    public final void setDesktop(final TDesktop desktop) {
+        if (this.desktop != null) {
+            this.desktop.onClose();
+        }
+        this.desktop = desktop;
+    }
+
+    /**
+     * Get the TDesktop instance.
+     *
+     * @return the desktop, or null if it is not set
+     */
+    public final TDesktop getDesktop() {
+        return desktop;
+    }
+
+    /**
+     * Get the current active window.
+     *
+     * @return the active window, or null if it is not set
+     */
+    public final TWindow getActiveWindow() {
+        return activeWindow;
+    }
+
+    /**
+     * Get a (shallow) copy of the window list.
+     *
+     * @return a copy of the list of windows for this application
+     */
+    public final List<TWindow> getAllWindows() {
+        List<TWindow> result = new LinkedList<TWindow>();
+        result.addAll(windows);
+        return result;
+    }
+
+    /**
+     * If true, focus follows mouse: windows automatically raised if the
+     * mouse passes over them.
+     */
+    private boolean focusFollowsMouse = false;
+
+    /**
+     * Get focusFollowsMouse flag.
+     *
+     * @return true if focus follows mouse: windows automatically raised if
+     * the mouse passes over them
+     */
+    public boolean getFocusFollowsMouse() {
+        return focusFollowsMouse;
+    }
+
+    /**
+     * Set focusFollowsMouse flag.
+     *
+     * @param focusFollowsMouse if true, focus follows mouse: windows
+     * automatically raised if the mouse passes over them
+     */
+    public void setFocusFollowsMouse(final boolean focusFollowsMouse) {
+        this.focusFollowsMouse = focusFollowsMouse;
+    }
+
     // ------------------------------------------------------------------------
     // General behavior -------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -557,6 +646,12 @@ public class TApplication implements Runnable {
 
         switch (backendType) {
         case SWING:
+            // The default SwingBackend is 80x25, 20 pt font.  If you want to
+            // change that, you can pass the extra arguments to the
+            // SwingBackend constructor here.  For example, if you wanted
+            // 90x30, 16 pt font:
+            //
+            // backend = new SwingBackend(this, 90, 30, 16);
             backend = new SwingBackend(this);
             break;
         case XTERM:
@@ -633,6 +728,7 @@ public class TApplication implements Runnable {
      */
     public TApplication(final Backend backend) {
         this.backend = backend;
+        backend.setListener(this);
         TApplicationImpl();
     }
 
@@ -650,6 +746,7 @@ public class TApplication implements Runnable {
         timers          = new LinkedList<TTimer>();
         accelerators    = new HashMap<TKeypress, TMenuItem>();
         menuItems       = new ArrayList<TMenuItem>();
+        desktop         = new TDesktop(this);
 
         // Setup the main consumer thread
         primaryEventHandler = new WidgetEventHandler(this, true);
@@ -714,17 +811,23 @@ public class TApplication implements Runnable {
         // Start with a clean screen
         getScreen().clear();
 
-        // Draw the background
-        CellAttributes background = theme.getColor("tapplication.background");
-        getScreen().putAll(GraphicsChars.HATCH, background);
+        // Draw the desktop
+        if (desktop != null) {
+            desktop.drawChildren();
+        }
 
         // Draw each window in reverse Z order
         List<TWindow> sorted = new LinkedList<TWindow>(windows);
         Collections.sort(sorted);
-        TWindow topLevel = sorted.get(0);
+        TWindow topLevel = null;
+        if (sorted.size() > 0) {
+            topLevel = sorted.get(0);
+        }
         Collections.reverse(sorted);
         for (TWindow window: sorted) {
-            window.drawChildren();
+            if (window.isShown()) {
+                window.drawChildren();
+            }
         }
 
         // Draw the blank menubar line - reset the screen clipping first so
@@ -768,7 +871,10 @@ public class TApplication implements Runnable {
         }
 
         // Draw the status bar of the top-level window
-        TStatusBar statusBar = topLevel.getStatusBar();
+        TStatusBar statusBar = null;
+        if (topLevel != null) {
+            statusBar = topLevel.getStatusBar();
+        }
         if (statusBar != null) {
             getScreen().resetClipping();
             statusBar.setWidth(getScreen().getWidth());
@@ -814,6 +920,13 @@ public class TApplication implements Runnable {
     // Main loop --------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Force this application to exit.
+     */
+    public void exit() {
+        quit = true;
+    }
+
     /**
      * Run this application until it exits.
      */
@@ -821,7 +934,7 @@ public class TApplication implements Runnable {
         while (!quit) {
             // Timeout is in milliseconds, so default timeout after 1 second
             // of inactivity.
-            long timeout = 0;
+            long timeout = 1000;
 
             // If I've got no updates to render, wait for something from the
             // backend or a timer.
@@ -951,20 +1064,11 @@ public class TApplication implements Runnable {
                 oldMouseX = 0;
                 oldMouseY = 0;
             }
-            return;
-        }
-
-        // Peek at the mouse position
-        if (event instanceof TMouseEvent) {
-            TMouseEvent mouse = (TMouseEvent) event;
-            synchronized (getScreen()) {
-                if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
-                    oldMouseX = mouseX;
-                    oldMouseY = mouseY;
-                    mouseX = mouse.getX();
-                    mouseY = mouse.getY();
-                }
+            if (desktop != null) {
+                desktop.setDimensions(0, 0, resize.getWidth(),
+                    resize.getHeight() - 1);
             }
+            return;
         }
 
         // Put into the main queue
@@ -989,6 +1093,14 @@ public class TApplication implements Runnable {
 
         // Peek at the mouse position
         if (event instanceof TMouseEvent) {
+            TMouseEvent mouse = (TMouseEvent) event;
+            if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
+                oldMouseX = mouseX;
+                oldMouseY = mouseY;
+                mouseX = mouse.getX();
+                mouseY = mouse.getY();
+            }
+
             // See if we need to switch focus to another window or the menu
             checkSwitchFocus((TMouseEvent) event);
         }
@@ -1038,13 +1150,12 @@ public class TApplication implements Runnable {
             // shortcutted by the active window, and if so dispatch the menu
             // event.
             boolean windowWillShortcut = false;
-            for (TWindow window: windows) {
-                if (window.isActive()) {
-                    if (window.isShortcutKeypress(keypress.getKey())) {
-                        // We do not process this key, it will be passed to
-                        // the window instead.
-                        windowWillShortcut = true;
-                    }
+            if (activeWindow != null) {
+                assert (activeWindow.isShown());
+                if (activeWindow.isShortcutKeypress(keypress.getKey())) {
+                    // We do not process this key, it will be passed to the
+                    // window instead.
+                    windowWillShortcut = true;
                 }
             }
 
@@ -1082,25 +1193,40 @@ public class TApplication implements Runnable {
         }
 
         // Dispatch events to the active window -------------------------------
-        for (TWindow window: windows) {
-            if (window.isActive()) {
-                if (event instanceof TMouseEvent) {
-                    TMouseEvent mouse = (TMouseEvent) event;
-                    // Convert the mouse relative x/y to window coordinates
-                    assert (mouse.getX() == mouse.getAbsoluteX());
-                    assert (mouse.getY() == mouse.getAbsoluteY());
-                    mouse.setX(mouse.getX() - window.getX());
-                    mouse.setY(mouse.getY() - window.getY());
-                }
-                if (debugEvents) {
-                    System.err.printf("TApplication dispatch event: %s\n",
-                        event);
+        boolean dispatchToDesktop = true;
+        TWindow window = activeWindow;
+        if (window != null) {
+            assert (window.isActive());
+            assert (window.isShown());
+            if (event instanceof TMouseEvent) {
+                TMouseEvent mouse = (TMouseEvent) event;
+                // Convert the mouse relative x/y to window coordinates
+                assert (mouse.getX() == mouse.getAbsoluteX());
+                assert (mouse.getY() == mouse.getAbsoluteY());
+                mouse.setX(mouse.getX() - window.getX());
+                mouse.setY(mouse.getY() - window.getY());
+
+                if (window.mouseWouldHit(mouse)) {
+                    dispatchToDesktop = false;
                 }
-                window.handleEvent(event);
-                break;
+            } else if (event instanceof TKeypressEvent) {
+                dispatchToDesktop = false;
+            }
+
+            if (debugEvents) {
+                System.err.printf("TApplication dispatch event: %s\n",
+                    event);
+            }
+            window.handleEvent(event);
+        }
+        if (dispatchToDesktop) {
+            // This event is fair game for the desktop to process.
+            if (desktop != null) {
+                desktop.handleEvent(event);
             }
         }
     }
+
     /**
      * Dispatch one event to the appropriate widget or application-level
      * event handler.  This is the secondary event handler used by certain
@@ -1110,6 +1236,17 @@ public class TApplication implements Runnable {
      * @see #primaryHandleEvent(TInputEvent event)
      */
     private void secondaryHandleEvent(final TInputEvent event) {
+        // Peek at the mouse position
+        if (event instanceof TMouseEvent) {
+            TMouseEvent mouse = (TMouseEvent) event;
+            if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
+                oldMouseX = mouseX;
+                oldMouseY = mouseY;
+                mouseX = mouse.getX();
+                mouseY = mouse.getY();
+            }
+        }
+
         secondaryEventReceiver.handleEvent(event);
     }
 
@@ -1178,12 +1315,186 @@ public class TApplication implements Runnable {
         for (TWindow window: windows) {
             window.onIdle();
         }
+        if (desktop != null) {
+            desktop.onIdle();
+        }
     }
 
     // ------------------------------------------------------------------------
     // TWindow management -----------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Return the total number of windows.
+     *
+     * @return the total number of windows
+     */
+    public final int windowCount() {
+        return windows.size();
+    }
+
+    /**
+     * Return the number of windows that are showing.
+     *
+     * @return the number of windows that are showing on screen
+     */
+    public final int shownWindowCount() {
+        int n = 0;
+        for (TWindow w: windows) {
+            if (w.isShown()) {
+                n++;
+            }
+        }
+        return n;
+    }
+
+    /**
+     * Return the number of windows that are hidden.
+     *
+     * @return the number of windows that are hidden
+     */
+    public final int hiddenWindowCount() {
+        int n = 0;
+        for (TWindow w: windows) {
+            if (w.isHidden()) {
+                n++;
+            }
+        }
+        return n;
+    }
+
+    /**
+     * Check if a window instance is in this application's window list.
+     *
+     * @param window window to look for
+     * @return true if this window is in the list
+     */
+    public final boolean hasWindow(final TWindow window) {
+        if (windows.size() == 0) {
+            return false;
+        }
+        for (TWindow w: windows) {
+            if (w == window) {
+                assert (window.getApplication() == this);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Activate a window: bring it to the top and have it receive events.
+     *
+     * @param window the window to become the new active window
+     */
+    public void activateWindow(final TWindow window) {
+        if (hasWindow(window) == false) {
+            /*
+             * Someone has a handle to a window I don't have.  Ignore this
+             * request.
+             */
+            return;
+        }
+
+        assert (windows.size() > 0);
+
+        if (window.isHidden()) {
+            // Unhiding will also activate.
+            showWindow(window);
+            return;
+        }
+        assert (window.isShown());
+
+        if (windows.size() == 1) {
+            assert (window == windows.get(0));
+            if (activeWindow == null) {
+                activeWindow = window;
+                window.setZ(0);
+                activeWindow.setActive(true);
+                activeWindow.onFocus();
+            }
+
+            assert (window.isActive());
+            assert (activeWindow == window);
+            return;
+        }
+
+        if (activeWindow == window) {
+            assert (window.isActive());
+
+            // Window is already active, do nothing.
+            return;
+        }
+
+        assert (!window.isActive());
+        if (activeWindow != null) {
+            assert (activeWindow.getZ() == 0);
+
+            activeWindow.onUnfocus();
+            activeWindow.setActive(false);
+            activeWindow.setZ(window.getZ());
+        }
+        activeWindow = window;
+        activeWindow.setZ(0);
+        activeWindow.setActive(true);
+        activeWindow.onFocus();
+        return;
+    }
+
+    /**
+     * Hide a window.
+     *
+     * @param window the window to hide
+     */
+    public void hideWindow(final TWindow window) {
+        if (hasWindow(window) == false) {
+            /*
+             * Someone has a handle to a window I don't have.  Ignore this
+             * request.
+             */
+            return;
+        }
+
+        assert (windows.size() > 0);
+
+        if (!window.hidden) {
+            if (window == activeWindow) {
+                if (shownWindowCount() > 1) {
+                    switchWindow(true);
+                } else {
+                    activeWindow = null;
+                    window.setActive(false);
+                    window.onUnfocus();
+                }
+            }
+            window.hidden = true;
+            window.onHide();
+        }
+    }
+
+    /**
+     * Show a window.
+     *
+     * @param window the window to show
+     */
+    public void showWindow(final TWindow window) {
+        if (hasWindow(window) == false) {
+            /*
+             * Someone has a handle to a window I don't have.  Ignore this
+             * request.
+             */
+            return;
+        }
+
+        assert (windows.size() > 0);
+
+        if (window.hidden) {
+            window.hidden = false;
+            window.onShow();
+            activateWindow(window);
+        }
+    }
+
     /**
      * Close window.  Note that the window's destructor is NOT called by this
      * method, instead the GC is assumed to do the cleanup.
@@ -1191,13 +1502,21 @@ public class TApplication implements Runnable {
      * @param window the window to remove
      */
     public final void closeWindow(final TWindow window) {
+        if (hasWindow(window) == false) {
+            /*
+             * Someone has a handle to a window I don't have.  Ignore this
+             * request.
+             */
+            return;
+        }
+
         synchronized (windows) {
             int z = window.getZ();
             window.setZ(-1);
             window.onUnfocus();
             Collections.sort(windows);
             windows.remove(0);
-            TWindow activeWindow = null;
+            activeWindow = null;
             for (TWindow w: windows) {
                 if (w.getZ() > z) {
                     w.setZ(w.getZ() - 1);
@@ -1233,6 +1552,13 @@ public class TApplication implements Runnable {
                 secondaryEventHandler.notify();
             }
         }
+
+        // Permit desktop to be active if it is the only thing left.
+        if (desktop != null) {
+            if (windows.size() == 0) {
+                desktop.setActive(true);
+            }
+        }
     }
 
     /**
@@ -1242,45 +1568,49 @@ public class TApplication implements Runnable {
      * otherwise switch to the previous window in the list
      */
     public final void switchWindow(final boolean forward) {
-        // Only switch if there are multiple windows
-        if (windows.size() < 2) {
+        // Only switch if there are multiple visible windows
+        if (shownWindowCount() < 2) {
             return;
         }
+        assert (activeWindow != null);
 
         synchronized (windows) {
 
             // Swap z/active between active window and the next in the list
             int activeWindowI = -1;
             for (int i = 0; i < windows.size(); i++) {
-                if (windows.get(i).isActive()) {
+                if (windows.get(i) == activeWindow) {
+                    assert (activeWindow.isActive());
                     activeWindowI = i;
                     break;
+                } else {
+                    assert (!windows.get(0).isActive());
                 }
             }
             assert (activeWindowI >= 0);
 
             // Do not switch if a window is modal
-            if (windows.get(activeWindowI).isModal()) {
+            if (activeWindow.isModal()) {
                 return;
             }
 
-            int nextWindowI;
-            if (forward) {
-                nextWindowI = (activeWindowI + 1) % windows.size();
-            } else {
-                if (activeWindowI == 0) {
-                    nextWindowI = windows.size() - 1;
+            int nextWindowI = activeWindowI;
+            for (;;) {
+                if (forward) {
+                    nextWindowI++;
+                    nextWindowI %= windows.size();
                 } else {
-                    nextWindowI = activeWindowI - 1;
+                    nextWindowI--;
+                    if (nextWindowI < 0) {
+                        nextWindowI = windows.size() - 1;
+                    }
                 }
-            }
-            windows.get(activeWindowI).setActive(false);
-            windows.get(activeWindowI).setZ(windows.get(nextWindowI).getZ());
-            windows.get(activeWindowI).onUnfocus();
-            windows.get(nextWindowI).setZ(0);
-            windows.get(nextWindowI).setActive(true);
-            windows.get(nextWindowI).onFocus();
 
+                if (windows.get(nextWindowI).isShown()) {
+                    activateWindow(windows.get(nextWindowI));
+                    break;
+                }
+            }
         } // synchronized (windows)
 
     }
@@ -1297,6 +1627,11 @@ public class TApplication implements Runnable {
             return;
         }
 
+        // Do not add the desktop to the window list.
+        if (window instanceof TDesktop) {
+            return;
+        }
+
         synchronized (windows) {
             // Do not allow a modal window to spawn a non-modal window.  If a
             // modal window is active, then this window will become modal
@@ -1304,18 +1639,24 @@ public class TApplication implements Runnable {
             if (modalWindowActive()) {
                 window.flags |= TWindow.MODAL;
                 window.flags |= TWindow.CENTERED;
+                window.hidden = false;
             }
-            for (TWindow w: windows) {
-                if (w.isActive()) {
-                    w.setActive(false);
-                    w.onUnfocus();
+            if (window.isShown()) {
+                for (TWindow w: windows) {
+                    if (w.isActive()) {
+                        w.setActive(false);
+                        w.onUnfocus();
+                    }
+                    w.setZ(w.getZ() + 1);
                 }
-                w.setZ(w.getZ() + 1);
             }
             windows.add(window);
-            window.setZ(0);
-            window.setActive(true);
-            window.onFocus();
+            if (window.isShown()) {
+                activeWindow = window;
+                activeWindow.setZ(0);
+                activeWindow.setActive(true);
+                activeWindow.onFocus();
+            }
 
             if (((window.flags & TWindow.CENTERED) == 0)
                 && smartWindowPlacement) {
@@ -1323,6 +1664,11 @@ public class TApplication implements Runnable {
                 doSmartPlacement(window);
             }
         }
+
+        // Desktop cannot be active over any other window.
+        if (desktop != null) {
+            desktop.setActive(false);
+        }
     }
 
     /**
@@ -1477,11 +1823,11 @@ public class TApplication implements Runnable {
                 continue;
             }
             for (int x = w.getX(); x < w.getX() + w.getWidth(); x++) {
-                if (x == width) {
+                if (x >= width) {
                     continue;
                 }
                 for (int y = w.getY(); y < w.getY() + w.getHeight(); y++) {
-                    if (y == height) {
+                    if (y >= height) {
                         continue;
                     }
                     overlapMatrix[x][y]++;
@@ -1524,11 +1870,11 @@ public class TApplication implements Runnable {
                 long newOverlapN = 0;
                 // Start by adding each new cell.
                 for (int wx = x; wx < x + window.getWidth(); wx++) {
-                    if (wx == width) {
+                    if (wx >= width) {
                         continue;
                     }
                     for (int wy = y; wy < y + window.getHeight(); wy++) {
-                        if (wy == height) {
+                        if (wy >= height) {
                             continue;
                         }
                         newMatrix[wx][wy]++;
@@ -1672,46 +2018,64 @@ public class TApplication implements Runnable {
             return;
         }
 
-        // Only switch if there are multiple windows
-        if (windows.size() < 2) {
+        // If a menu is still active, don't switch windows
+        if (activeMenu != null) {
             return;
         }
 
-        // Switch on the upclick
-        if (mouse.getType() != TMouseEvent.Type.MOUSE_UP) {
+        // Only switch if there are multiple windows
+        if (windows.size() < 2) {
             return;
         }
 
-        synchronized (windows) {
-            Collections.sort(windows);
-            if (windows.get(0).isModal()) {
-                // Modal windows don't switch
-                return;
-            }
+        if (((focusFollowsMouse == true)
+                && (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION))
+            || (mouse.getType() == TMouseEvent.Type.MOUSE_UP)
+        ) {
+            synchronized (windows) {
+                Collections.sort(windows);
+                if (windows.get(0).isModal()) {
+                    // Modal windows don't switch
+                    return;
+                }
 
-            for (TWindow window: windows) {
-                assert (!window.isModal());
-                if (window.mouseWouldHit(mouse)) {
-                    if (window == windows.get(0)) {
-                        // Clicked on the same window, nothing to do
-                        return;
+                for (TWindow window: windows) {
+                    assert (!window.isModal());
+
+                    if (window.isHidden()) {
+                        assert (!window.isActive());
+                        continue;
                     }
 
-                    // We will be switching to another window
-                    assert (windows.get(0).isActive());
-                    assert (!window.isActive());
-                    windows.get(0).onUnfocus();
-                    windows.get(0).setActive(false);
-                    windows.get(0).setZ(window.getZ());
-                    window.setZ(0);
-                    window.setActive(true);
-                    window.onFocus();
-                    return;
+                    if (window.mouseWouldHit(mouse)) {
+                        if (window == windows.get(0)) {
+                            // Clicked on the same window, nothing to do
+                            assert (window.isActive());
+                            return;
+                        }
+
+                        // We will be switching to another window
+                        assert (windows.get(0).isActive());
+                        assert (windows.get(0) == activeWindow);
+                        assert (!window.isActive());
+                        activeWindow.onUnfocus();
+                        activeWindow.setActive(false);
+                        activeWindow.setZ(window.getZ());
+                        activeWindow = window;
+                        window.setZ(0);
+                        window.setActive(true);
+                        window.onFocus();
+                        return;
+                    }
                 }
             }
+
+            // Clicked on the background, nothing to do
+            return;
         }
 
-        // Clicked on the background, nothing to do
+        // Nothing to do: this isn't a mouse up, or focus isn't following
+        // mouse.
         return;
     }
 
@@ -1729,6 +2093,53 @@ public class TApplication implements Runnable {
         }
     }
 
+    /**
+     * Get a (shallow) copy of the menu list.
+     *
+     * @return a copy of the menu list
+     */
+    public final List<TMenu> getAllMenus() {
+        return new LinkedList<TMenu>(menus);
+    }
+
+    /**
+     * Add a top-level menu to the list.
+     *
+     * @param menu the menu to add
+     * @throws IllegalArgumentException if the menu is already used in
+     * another TApplication
+     */
+    public final void addMenu(final TMenu menu) {
+        if ((menu.getApplication() != null)
+            && (menu.getApplication() != this)
+        ) {
+            throw new IllegalArgumentException("Menu " + menu + " is already " +
+                "part of application " + menu.getApplication());
+        }
+        closeMenu();
+        menus.add(menu);
+        recomputeMenuX();
+    }
+
+    /**
+     * Remove a top-level menu from the list.
+     *
+     * @param menu the menu to remove
+     * @throws IllegalArgumentException if the menu is already used in
+     * another TApplication
+     */
+    public final void removeMenu(final TMenu menu) {
+        if ((menu.getApplication() != null)
+            && (menu.getApplication() != this)
+        ) {
+            throw new IllegalArgumentException("Menu " + menu + " is already " +
+                "part of application " + menu.getApplication());
+        }
+        closeMenu();
+        menus.remove(menu);
+        recomputeMenuX();
+    }
+
     /**
      * Turn off a sub-menu.
      */
@@ -2261,4 +2672,68 @@ public class TApplication implements Runnable {
         return box.getFilename();
     }
 
+    /**
+     * Convenience function to create a new window and make it active.
+     * Window will be located at (0, 0).
+     *
+     * @param title window title, will be centered along the top border
+     * @param width width of window
+     * @param height height of window
+     */
+    public final TWindow addWindow(final String title, final int width,
+        final int height) {
+
+        TWindow window = new TWindow(this, title, 0, 0, width, height);
+        return window;
+    }
+    /**
+     * Convenience function to create a new window and make it active.
+     * Window will be located at (0, 0).
+     *
+     * @param title window title, will be centered along the top border
+     * @param width width of window
+     * @param height height of window
+     * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
+     */
+    public final TWindow addWindow(final String title,
+        final int width, final int height, final int flags) {
+
+        TWindow window = new TWindow(this, title, 0, 0, width, height, flags);
+        return window;
+    }
+
+    /**
+     * Convenience function to create a new window and make it active.
+     *
+     * @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 final TWindow addWindow(final String title,
+        final int x, final int y, final int width, final int height) {
+
+        TWindow window = new TWindow(this, title, x, y, width, height);
+        return window;
+    }
+
+    /**
+     * Convenience function to create a new window and make it active.
+     *
+     * @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 final TWindow addWindow(final String title,
+        final int x, final int y, final int width, final int height,
+        final int flags) {
+
+        TWindow window = new TWindow(this, title, x, y, width, height, flags);
+        return window;
+    }
+
 }