Merge branch 'subtree'
[fanfix.git] / src / jexer / TApplication.java
index dba59d7d7eaab7bc3f8f4c2e3f9866c1194ce713..28e35091ded6e1ef006190574e945c0426c41057 100644 (file)
@@ -29,6 +29,7 @@
 package jexer;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.InputStream;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -62,6 +63,8 @@ import jexer.backend.Screen;
 import jexer.backend.SwingBackend;
 import jexer.backend.ECMA48Backend;
 import jexer.backend.TWindowBackend;
+import jexer.help.HelpFile;
+import jexer.help.Topic;
 import jexer.menu.TMenu;
 import jexer.menu.TMenuItem;
 import jexer.menu.TSubMenu;
@@ -164,16 +167,6 @@ public class TApplication implements Runnable {
      */
     private int mouseY;
 
-    /**
-     * Old version of mouse coordinate X.
-     */
-    private int oldMouseX;
-
-    /**
-     * Old version mouse coordinate Y.
-     */
-    private int oldMouseY;
-
     /**
      * Old drawn version of mouse coordinate X.
      */
@@ -246,11 +239,6 @@ public class TApplication implements Runnable {
      */
     private List<TWindow> windows;
 
-    /**
-     * The currently acive window.
-     */
-    private TWindow activeWindow = null;
-
     /**
      * Timers that are being ticked.
      */
@@ -361,6 +349,16 @@ public class TApplication implements Runnable {
      */
     private int screenSelectionY1;
 
+    /**
+     * The help file data.  Note package private access.
+     */
+    HelpFile helpFile;
+
+    /**
+     * The stack of help topics.  Note package private access.
+     */
+    ArrayList<Topic> helpTopics = new ArrayList<Topic>();
+
     /**
      * WidgetEventHandler is the main event consumer loop.  There are at most
      * two such threads in existence: the primary for normal case and a
@@ -809,6 +807,27 @@ public class TApplication implements Runnable {
             }
         }
 
+        // Load the help system
+        invokeLater(new Runnable() {
+            /*
+             * This isn't the best solution.  But basically if a TApplication
+             * subclass constructor throws and needs to use TExceptionDialog,
+             * it may end up at the bottom of the window stack with a bunch
+             * of modal windows on top of it if said constructors spawn their
+             * windows also via invokeLater().  But if they don't do that,
+             * and instead just conventionally construct their windows, then
+             * this exception dialog will end up on top where it should be.
+             */
+            public void run() {
+                try {
+                    ClassLoader loader = Thread.currentThread().getContextClassLoader();
+                    helpFile = new HelpFile();
+                    helpFile.load(loader.getResourceAsStream("help.xml"));
+                } catch (Exception e) {
+                    new TExceptionDialog(TApplication.this, e);
+                }
+            }
+        });
     }
 
     // ------------------------------------------------------------------------
@@ -940,6 +959,15 @@ public class TApplication implements Runnable {
             return true;
         }
 
+        if (command.equals(cmHelp)) {
+            if (getActiveWindow() != null) {
+                new THelpWindow(this, getActiveWindow().getHelpTopic());
+            } else {
+                new THelpWindow(this);
+            }
+            return true;
+        }
+
         if (command.equals(cmShell)) {
             openTerminal(0, 0, TWindow.RESIZABLE);
             return true;
@@ -991,6 +1019,62 @@ public class TApplication implements Runnable {
             return true;
         }
 
+        if (menu.getId() == TMenu.MID_HELP_HELP) {
+            new THelpWindow(this, THelpWindow.HELP_HELP);
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_CONTENTS) {
+            new THelpWindow(this, helpFile.getTableOfContents());
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_INDEX) {
+            new THelpWindow(this, helpFile.getIndex());
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_SEARCH) {
+            TInputBox inputBox = inputBox(i18n.
+                getString("searchHelpInputBoxTitle"),
+                i18n.getString("searchHelpInputBoxCaption"), "",
+                TInputBox.Type.OKCANCEL);
+            if (inputBox.isOk()) {
+                new THelpWindow(this,
+                    helpFile.getSearchResults(inputBox.getText()));
+            }
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_PREVIOUS) {
+            if (helpTopics.size() > 1) {
+                Topic previous = helpTopics.remove(helpTopics.size() - 2);
+                helpTopics.remove(helpTopics.size() - 1);
+                new THelpWindow(this, previous);
+            } else {
+                new THelpWindow(this, helpFile.getTableOfContents());
+            }
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_ACTIVE_FILE) {
+            try {
+                List<String> filters = new ArrayList<String>();
+                filters.add("^.*\\.[Xx][Mm][Ll]$");
+                String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN,
+                    filters);
+                if (filename != null) {
+                    helpTopics = new ArrayList<Topic>();
+                    helpFile = new HelpFile();
+                    helpFile.load(new FileInputStream(filename));
+                }
+            } catch (Exception e) {
+                // Show this exception to the user.
+                new TExceptionDialog(this, e);
+            }
+            return true;
+        }
+
         if (menu.getId() == TMenu.MID_SHELL) {
             openTerminal(0, 0, TWindow.RESIZABLE);
             return true;
@@ -1092,6 +1176,7 @@ public class TApplication implements Runnable {
         // See if we need to enable/disable the edit menu.
         EditMenuUser widget = null;
         if (activeMenu == null) {
+            TWindow activeWindow = getActiveWindow();
             if (activeWindow != null) {
                 if (activeWindow.getActiveChild() instanceof EditMenuUser) {
                     widget = (EditMenuUser) activeWindow.getActiveChild();
@@ -1196,8 +1281,6 @@ public class TApplication implements Runnable {
                     }
                     mouseX = 0;
                     mouseY = 0;
-                    oldMouseX = 0;
-                    oldMouseY = 0;
                 }
                 if (desktop != null) {
                     desktop.setDimensions(0, desktopTop, resize.getWidth(),
@@ -1275,8 +1358,6 @@ public class TApplication implements Runnable {
             }
 
             if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
-                oldMouseX = mouseX;
-                oldMouseY = mouseY;
                 mouseX = mouse.getX();
                 mouseY = mouse.getY();
             } else {
@@ -1353,6 +1434,7 @@ public class TApplication implements Runnable {
             // shortcutted by the active window, and if so dispatch the menu
             // event.
             boolean windowWillShortcut = false;
+            TWindow activeWindow = getActiveWindow();
             if (activeWindow != null) {
                 assert (activeWindow.isShown());
                 if (activeWindow.isShortcutKeypress(keypress.getKey())) {
@@ -1397,7 +1479,7 @@ public class TApplication implements Runnable {
 
         // Dispatch events to the active window -------------------------------
         boolean dispatchToDesktop = true;
-        TWindow window = activeWindow;
+        TWindow window = getActiveWindow();
         if (window != null) {
             assert (window.isActive());
             assert (window.isShown());
@@ -1465,8 +1547,6 @@ public class TApplication implements Runnable {
 
             TMouseEvent mouse = (TMouseEvent) event;
             if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
-                oldMouseX = mouseX;
-                oldMouseY = mouseY;
                 mouseX = mouse.getX();
                 mouseY = mouse.getY();
             } else {
@@ -1593,14 +1673,17 @@ public class TApplication implements Runnable {
             desktop.onIdle();
         }
 
-        // Run any invokeLaters
+        // Run any invokeLaters.  We make a copy, and run that, because one
+        // of these Runnables might add call TApplication.invokeLater().
+        List<Runnable> invokes = new ArrayList<Runnable>();
         synchronized (invokeLaters) {
-            for (Runnable invoke: invokeLaters) {
-                invoke.run();
-            }
+            invokes.addAll(invokeLaters);
             invokeLaters.clear();
-            doRepaint();
         }
+        for (Runnable invoke: invokes) {
+            invoke.run();
+        }
+        doRepaint();
 
     }
 
@@ -1767,7 +1850,12 @@ public class TApplication implements Runnable {
      * @return the active window, or null if it is not set
      */
     public final TWindow getActiveWindow() {
-        return activeWindow;
+        for (TWindow window: windows) {
+            if (window.isShown() && window.isActive()) {
+                return window;
+            }
+        }
+        return null;
     }
 
     /**
@@ -1859,6 +1947,7 @@ public class TApplication implements Runnable {
      * @param y row position
      */
     private void drawTextMouse(final int x, final int y) {
+        TWindow activeWindow = getActiveWindow();
 
         if (debugThreads) {
             System.err.printf("%d %s drawTextMouse() %d %d\n",
@@ -2058,7 +2147,9 @@ public class TApplication implements Runnable {
             // Draw the status bar of the top-level window
             TStatusBar statusBar = null;
             if (topLevel != null) {
-                statusBar = topLevel.getStatusBar();
+                if (topLevel.isShown()) {
+                    statusBar = topLevel.getStatusBar();
+                }
             }
             if (statusBar != null) {
                 getScreen().resetClipping();
@@ -2246,7 +2337,7 @@ public class TApplication implements Runnable {
      *
      * @param window the window to become the new active window
      */
-    public void activateWindow(final TWindow window) {
+    public final void activateWindow(final TWindow window) {
         if (hasWindow(window) == false) {
             /*
              * Someone has a handle to a window I don't have.  Ignore this
@@ -2255,68 +2346,61 @@ public class TApplication implements Runnable {
             return;
         }
 
-        // Whatever window might be moving/dragging, stop it now.
-        for (TWindow w: windows) {
-            if (w.inMovements()) {
-                w.stopMovements();
-            }
+        if (modalWindowActive() && !window.isModal()) {
+            // Do not activate a non-modal on top of a modal.
+            return;
         }
 
-        assert (windows.size() > 0);
+        synchronized (windows) {
+            // Whatever window might be moving/dragging, stop it now.
+            for (TWindow w: windows) {
+                if (w.inMovements()) {
+                    w.stopMovements();
+                }
+            }
 
-        if (window.isHidden()) {
-            // Unhiding will also activate.
-            showWindow(window);
-            return;
-        }
-        assert (window.isShown());
+            assert (windows.size() > 0);
 
-        if (windows.size() == 1) {
-            assert (window == windows.get(0));
-            if (activeWindow == null) {
-                activeWindow = window;
-                window.setZ(0);
-                activeWindow.setActive(true);
-                activeWindow.onFocus();
+            if (window.isHidden()) {
+                // Unhiding will also activate.
+                showWindow(window);
+                return;
             }
+            assert (window.isShown());
 
-            assert (window.isActive());
-            assert (activeWindow == window);
-            return;
-        }
+            if (windows.size() == 1) {
+                assert (window == windows.get(0));
+                window.setZ(0);
+                window.setActive(true);
+                window.onFocus();
+                return;
+            }
 
-        if (activeWindow == window) {
-            assert (window.isActive());
+            if (getActiveWindow() == window) {
+                assert (window.isActive());
 
-            // Window is already active, do nothing.
-            return;
-        }
+                // Window is already active, do nothing.
+                return;
+            }
 
-        assert (!window.isActive());
-        if (activeWindow != null) {
-            activeWindow.setActive(false);
+            assert (!window.isActive());
 
-            // Increment every window Z that is on top of window
+            window.setZ(-1);
+            Collections.sort(windows);
+            int newZ = 0;
             for (TWindow w: windows) {
-                if (w == window) {
-                    continue;
-                }
-                if (w.getZ() < window.getZ()) {
-                    w.setZ(w.getZ() + 1);
+                w.setZ(newZ);
+                newZ++;
+                if ((w != window) && w.isActive()) {
+                    w.onUnfocus();
                 }
+                w.setActive(false);
             }
+            window.setActive(true);
+            window.onFocus();
+
+        } // synchronized (windows)
 
-            // Unset activeWindow now before unfocus, so that a window
-            // lifecycle change inside onUnfocus() doesn't call
-            // switchWindow() and lead to a stack overflow.
-            TWindow oldActiveWindow = activeWindow;
-            activeWindow = null;
-            oldActiveWindow.onUnfocus();
-        }
-        activeWindow = window;
-        activeWindow.setZ(0);
-        activeWindow.setActive(true);
-        activeWindow.onFocus();
         return;
     }
 
@@ -2334,28 +2418,39 @@ public class TApplication implements Runnable {
             return;
         }
 
-        // Whatever window might be moving/dragging, stop it now.
-        for (TWindow w: windows) {
-            if (w.inMovements()) {
-                w.stopMovements();
+        synchronized (windows) {
+
+            // Whatever window might be moving/dragging, stop it now.
+            for (TWindow w: windows) {
+                if (w.inMovements()) {
+                    w.stopMovements();
+                }
             }
-        }
 
-        assert (windows.size() > 0);
+            assert (windows.size() > 0);
 
-        if (!window.hidden) {
-            if (window == activeWindow) {
-                if (shownWindowCount() > 1) {
-                    switchWindow(true);
-                } else {
-                    activeWindow = null;
-                    window.setActive(false);
-                    window.onUnfocus();
-                }
+            if (window.hidden) {
+                return;
             }
+
+            window.setActive(false);
             window.hidden = true;
             window.onHide();
-        }
+
+            TWindow activeWindow = null;
+            for (TWindow w: windows) {
+                if (w.isShown()) {
+                    activeWindow = w;
+                    break;
+                }
+            }
+            assert (activeWindow != window);
+            if (activeWindow != null) {
+                activateWindow(activeWindow);
+            }
+
+        } // synchronized (windows)
+
     }
 
     /**
@@ -2372,25 +2467,16 @@ public class TApplication implements Runnable {
             return;
         }
 
-        // Whatever window might be moving/dragging, stop it now.
-        for (TWindow w: windows) {
-            if (w.inMovements()) {
-                w.stopMovements();
-            }
-        }
-
-        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.
+     * Close window.
      *
      * @param window the window to remove
      */
@@ -2408,23 +2494,16 @@ public class TApplication implements Runnable {
         window.onPreClose();
 
         synchronized (windows) {
-            // Whatever window might be moving/dragging, stop it now.
-            for (TWindow w: windows) {
-                if (w.inMovements()) {
-                    w.stopMovements();
-                }
-            }
 
-            int z = window.getZ();
-            window.setZ(-1);
+            window.stopMovements();
             window.onUnfocus();
             windows.remove(window);
             Collections.sort(windows);
-            activeWindow = null;
-            int newZ = 0;
-            boolean foundNextWindow = false;
 
+            TWindow nextWindow = null;
+            int newZ = 0;
             for (TWindow w: windows) {
+                w.stopMovements();
                 w.setZ(newZ);
                 newZ++;
 
@@ -2432,22 +2511,22 @@ public class TApplication implements Runnable {
                 if (w.isHidden()) {
                     continue;
                 }
-
-                if (foundNextWindow == false) {
-                    foundNextWindow = true;
-                    w.setActive(true);
-                    w.onFocus();
-                    assert (activeWindow == null);
-                    activeWindow = w;
-                    continue;
+                if (nextWindow == null) {
+                    nextWindow = w;
+                } else {
+                    if (w.isActive()) {
+                        w.setActive(false);
+                        w.onUnfocus();
+                    }
                 }
+            }
 
-                if (w.isActive()) {
-                    w.setActive(false);
-                    w.onUnfocus();
-                }
+            if (nextWindow != null) {
+                nextWindow.setActive(true);
+                nextWindow.onFocus();
             }
-        }
+
+        } // synchronized (windows)
 
         // Perform window cleanup
         window.onClose();
@@ -2465,7 +2544,8 @@ public class TApplication implements Runnable {
             synchronized (secondaryEventHandler) {
                 secondaryEventHandler.notify();
             }
-        }
+
+        } // synchronized (windows)
 
         // Permit desktop to be active if it is the only thing left.
         if (desktop != null) {
@@ -2486,53 +2566,50 @@ public class TApplication implements Runnable {
         if (shownWindowCount() < 2) {
             return;
         }
-        assert (activeWindow != null);
+
+        if (modalWindowActive()) {
+            // Do not switch if a window is modal
+            return;
+        }
 
         synchronized (windows) {
-            // Whatever window might be moving/dragging, stop it now.
-            for (TWindow w: windows) {
-                if (w.inMovements()) {
-                    w.stopMovements();
-                }
-            }
 
-            // 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) == activeWindow) {
-                    assert (activeWindow.isActive());
-                    activeWindowI = i;
-                    break;
+            TWindow window = windows.get(0);
+            do {
+                assert (window != null);
+                if (forward) {
+                    window.setZ(windows.size());
                 } else {
-                    assert (!windows.get(0).isActive());
+                    TWindow lastWindow = windows.get(windows.size() - 1);
+                    lastWindow.setZ(-1);
                 }
-            }
-            assert (activeWindowI >= 0);
-
-            // Do not switch if a window is modal
-            if (activeWindow.isModal()) {
-                return;
-            }
 
-            int nextWindowI = activeWindowI;
-            for (;;) {
-                if (forward) {
-                    nextWindowI++;
-                    nextWindowI %= windows.size();
-                } else {
-                    nextWindowI--;
-                    if (nextWindowI < 0) {
-                        nextWindowI = windows.size() - 1;
-                    }
+                Collections.sort(windows);
+                int newZ = 0;
+                for (TWindow w: windows) {
+                    w.setZ(newZ);
+                    newZ++;
                 }
 
-                if (windows.get(nextWindowI).isShown()) {
-                    activateWindow(windows.get(nextWindowI));
-                    break;
+                window = windows.get(0);
+            } while (!window.isShown());
+
+            // The next visible window is now on top.  Renumber the list.
+            for (TWindow w: windows) {
+                w.stopMovements();
+                if ((w != window) && w.isActive()) {
+                    assert (w.isShown());
+                    w.setActive(false);
+                    w.onUnfocus();
                 }
             }
-        } // synchronized (windows)
 
+            // Next visible window is on top.
+            assert (window.isShown());
+            window.setActive(true);
+            window.onFocus();
+
+        } // synchronized (windows)
     }
 
     /**
@@ -2582,13 +2659,13 @@ public class TApplication implements Runnable {
                     }
                     w.setZ(w.getZ() + 1);
                 }
-            }
-            windows.add(window);
-            if (window.isShown()) {
-                activeWindow = window;
-                activeWindow.setZ(0);
-                activeWindow.setActive(true);
-                activeWindow.onFocus();
+                window.setZ(0);
+                window.setActive(true);
+                window.onFocus();
+                windows.add(0, window);
+            } else {
+                window.setZ(windows.size());
+                windows.add(window);
             }
 
             if (((window.flags & TWindow.CENTERED) == 0)
@@ -2605,6 +2682,7 @@ public class TApplication implements Runnable {
         if (desktop != null) {
             desktop.setActive(false);
         }
+
     }
 
     /**
@@ -2632,6 +2710,7 @@ public class TApplication implements Runnable {
      * @return true if the active window is overriding the menu
      */
     private boolean overrideMenuWindowActive() {
+        TWindow activeWindow = getActiveWindow();
         if (activeWindow != null) {
             if (activeWindow.hasOverriddenMenu()) {
                 return true;
@@ -3002,7 +3081,6 @@ public class TApplication implements Runnable {
             || (mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
         ) {
             synchronized (windows) {
-                Collections.sort(windows);
                 if (windows.get(0).isModal()) {
                     // Modal windows don't switch
                     return;
@@ -3017,25 +3095,7 @@ public class TApplication implements Runnable {
                     }
 
                     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());
-                        if (activeWindow != null) {
-                            activeWindow.onUnfocus();
-                            activeWindow.setActive(false);
-                            activeWindow.setZ(window.getZ());
-                        }
-                        activeWindow = window;
-                        window.setZ(0);
-                        window.setActive(true);
-                        window.onFocus();
+                        activateWindow(window);
                         return;
                     }
                 }
@@ -3370,6 +3430,9 @@ public class TApplication implements Runnable {
      */
     public final TMenu addEditMenu() {
         TMenu editMenu = addMenu(i18n.getString("editMenuTitle"));
+        editMenu.addDefaultItem(TMenu.MID_UNDO, false);
+        editMenu.addDefaultItem(TMenu.MID_REDO, false);
+        editMenu.addSeparator();
         editMenu.addDefaultItem(TMenu.MID_CUT, false);
         editMenu.addDefaultItem(TMenu.MID_COPY, false);
         editMenu.addDefaultItem(TMenu.MID_PASTE, false);