Fix bounds check
[nikiroo-utils.git] / src / jexer / TApplication.java
index 105f1ce97cfa5a047e9502a1c8bf9877d6200fa8..e319a4df410dba7ae7c7ab552dfa1d5e824fb47d 100644 (file)
  */
 package jexer;
 
+import java.io.File;
 import java.io.InputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.io.Reader;
 import java.io.UnsupportedEncodingException;
+import java.text.MessageFormat;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
@@ -41,6 +43,7 @@ import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.ResourceBundle;
 
 import jexer.bits.CellAttributes;
 import jexer.bits.ColorTheme;
@@ -68,6 +71,11 @@ import static jexer.TKeypress.*;
  */
 public class TApplication implements Runnable {
 
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(TApplication.class.getName());
+
     // ------------------------------------------------------------------------
     // Public constants -------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -275,6 +283,10 @@ public class TApplication implements Runnable {
      * Wake the sleeping active event handler.
      */
     private void wakeEventHandler() {
+        if (!started) {
+            return;
+        }
+
         if (secondaryEventHandler != null) {
             synchronized (secondaryEventHandler) {
                 secondaryEventHandler.notify();
@@ -342,6 +354,17 @@ public class TApplication implements Runnable {
      */
     private int oldMouseY;
 
+    /**
+     * The last mouse up click time, used to determine if this is a mouse
+     * double-click.
+     */
+    private long lastMouseUpTime;
+
+    /**
+     * The amount of millis between mouse up events to assume a double-click.
+     */
+    private long doubleClickTime = 250;
+
     /**
      * Event queue that is filled by run().
      */
@@ -407,6 +430,11 @@ public class TApplication implements Runnable {
      */
     private List<TTimer> timers;
 
+    /**
+     * When true, the application has been started.
+     */
+    private volatile boolean started = false;
+
     /**
      * When true, exit the application.
      */
@@ -537,8 +565,9 @@ public class TApplication implements Runnable {
      * Display the about dialog.
      */
     protected void showAboutDialog() {
-        messageBox("About", "Jexer Version " +
-            this.getClass().getPackage().getImplementationVersion(),
+        messageBox(i18n.getString("aboutDialogTitle"),
+            MessageFormat.format(i18n.getString("aboutDialogText"),
+                this.getClass().getPackage().getImplementationVersion()),
             TMessageBox.Type.OK);
     }
 
@@ -732,6 +761,8 @@ public class TApplication implements Runnable {
      * Draw everything.
      */
     private void drawAll() {
+        boolean menuIsActive = false;
+
         if (debugThreads) {
             System.err.printf("%d %s drawAll() enter\n",
                 System.currentTimeMillis(), Thread.currentThread());
@@ -799,6 +830,7 @@ public class TApplication implements Runnable {
             CellAttributes menuColor;
             CellAttributes menuMnemonicColor;
             if (menu.isActive()) {
+                menuIsActive = true;
                 menuColor = theme.getColor("tmenu.highlighted");
                 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
                 topLevel = menu;
@@ -851,22 +883,24 @@ public class TApplication implements Runnable {
         oldMouseY = mouseY;
 
         // Place the cursor if it is visible
-        TWidget activeWidget = null;
-        if (sorted.size() > 0) {
-            activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
-            if (activeWidget.isCursorVisible()) {
-                if ((activeWidget.getCursorAbsoluteY() < desktopBottom)
-                    && (activeWidget.getCursorAbsoluteY() > desktopTop)
-                ) {
-                    getScreen().putCursor(true,
-                        activeWidget.getCursorAbsoluteX(),
-                        activeWidget.getCursorAbsoluteY());
-                    cursor = true;
-                } else {
-                    getScreen().putCursor(false,
-                        activeWidget.getCursorAbsoluteX(),
-                        activeWidget.getCursorAbsoluteY());
-                    cursor = false;
+        if (!menuIsActive) {
+            TWidget activeWidget = null;
+            if (sorted.size() > 0) {
+                activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
+                if (activeWidget.isCursorVisible()) {
+                    if ((activeWidget.getCursorAbsoluteY() < desktopBottom)
+                        && (activeWidget.getCursorAbsoluteY() > desktopTop)
+                    ) {
+                        getScreen().putCursor(true,
+                            activeWidget.getCursorAbsoluteX(),
+                            activeWidget.getCursorAbsoluteY());
+                        cursor = true;
+                    } else {
+                        getScreen().putCursor(false,
+                            activeWidget.getCursorAbsoluteX(),
+                            activeWidget.getCursorAbsoluteY());
+                        cursor = false;
+                    }
                 }
             }
         }
@@ -906,6 +940,8 @@ public class TApplication implements Runnable {
         primaryEventHandler = new WidgetEventHandler(this, true);
         (new Thread(primaryEventHandler)).start();
 
+        started = true;
+
         while (!quit) {
             synchronized (this) {
                 boolean doWait = false;
@@ -1030,6 +1066,12 @@ public class TApplication implements Runnable {
                     desktop.setDimensions(0, 0, resize.getWidth(),
                         resize.getHeight() - 1);
                 }
+
+                // Change menu edges if needed.
+                recomputeMenuX();
+
+                // We are dirty, redraw the screen.
+                doRepaint();
                 return;
             }
 
@@ -1051,6 +1093,7 @@ public class TApplication implements Runnable {
         if (debugEvents) {
             System.err.printf("Handle event: %s\n", event);
         }
+        TMouseEvent doubleClick = null;
 
         // Special application-wide events -----------------------------------
 
@@ -1062,6 +1105,25 @@ public class TApplication implements Runnable {
                 oldMouseY = mouseY;
                 mouseX = mouse.getX();
                 mouseY = mouse.getY();
+            } else {
+                if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
+                    if ((mouse.getTime().getTime() - lastMouseUpTime) <
+                        doubleClickTime) {
+
+                        // This is a double-click.
+                        doubleClick = new TMouseEvent(TMouseEvent.Type.
+                            MOUSE_DOUBLE_CLICK,
+                            mouse.getX(), mouse.getY(),
+                            mouse.getAbsoluteX(), mouse.getAbsoluteY(),
+                            mouse.isMouse1(), mouse.isMouse2(),
+                            mouse.isMouse3(),
+                            mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+
+                    } else {
+                        // The first click of a potential double-click.
+                        lastMouseUpTime = mouse.getTime().getTime();
+                    }
+                }
             }
 
             // See if we need to switch focus to another window or the menu
@@ -1169,6 +1231,11 @@ public class TApplication implements Runnable {
                 mouse.setX(mouse.getX() - window.getX());
                 mouse.setY(mouse.getY() - window.getY());
 
+                if (doubleClick != null) {
+                    doubleClick.setX(doubleClick.getX() - window.getX());
+                    doubleClick.setY(doubleClick.getY() - window.getY());
+                }
+
                 if (window.mouseWouldHit(mouse)) {
                     dispatchToDesktop = false;
                 }
@@ -1181,11 +1248,17 @@ public class TApplication implements Runnable {
                     event);
             }
             window.handleEvent(event);
+            if (doubleClick != null) {
+                window.handleEvent(doubleClick);
+            }
         }
         if (dispatchToDesktop) {
             // This event is fair game for the desktop to process.
             if (desktop != null) {
                 desktop.handleEvent(event);
+                if (doubleClick != null) {
+                    desktop.handleEvent(doubleClick);
+                }
             }
         }
     }
@@ -1199,6 +1272,8 @@ public class TApplication implements Runnable {
      * @see #primaryHandleEvent(TInputEvent event)
      */
     private void secondaryHandleEvent(final TInputEvent event) {
+        TMouseEvent doubleClick = null;
+
         // Peek at the mouse position
         if (event instanceof TMouseEvent) {
             TMouseEvent mouse = (TMouseEvent) event;
@@ -1207,10 +1282,32 @@ public class TApplication implements Runnable {
                 oldMouseY = mouseY;
                 mouseX = mouse.getX();
                 mouseY = mouse.getY();
+            } else {
+                if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
+                    if ((mouse.getTime().getTime() - lastMouseUpTime) <
+                        doubleClickTime) {
+
+                        // This is a double-click.
+                        doubleClick = new TMouseEvent(TMouseEvent.Type.
+                            MOUSE_DOUBLE_CLICK,
+                            mouse.getX(), mouse.getY(),
+                            mouse.getAbsoluteX(), mouse.getAbsoluteY(),
+                            mouse.isMouse1(), mouse.isMouse2(),
+                            mouse.isMouse3(),
+                            mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+
+                    } else {
+                        // The first click of a potential double-click.
+                        lastMouseUpTime = mouse.getTime().getTime();
+                    }
+                }
             }
         }
 
         secondaryEventReceiver.handleEvent(event);
+        if (doubleClick != null) {
+            secondaryEventReceiver.handleEvent(doubleClick);
+        }
     }
 
     /**
@@ -1411,9 +1508,15 @@ public class TApplication implements Runnable {
         if (activeWindow != null) {
             assert (activeWindow.getZ() == 0);
 
-            activeWindow.onUnfocus();
             activeWindow.setActive(false);
             activeWindow.setZ(window.getZ());
+
+            // 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);
@@ -1838,10 +1941,16 @@ public class TApplication implements Runnable {
                 continue;
             }
             for (int x = w.getX(); x < w.getX() + w.getWidth(); x++) {
+                if (x < 0) {
+                    continue;
+                }
                 if (x >= width) {
                     continue;
                 }
                 for (int y = w.getY(); y < w.getY() + w.getHeight(); y++) {
+                    if (y < 0) {
+                        continue;
+                    }
                     if (y >= height) {
                         continue;
                     }
@@ -1990,8 +2099,8 @@ public class TApplication implements Runnable {
 
             // They selected the menu, go activate it
             for (TMenu menu: menus) {
-                if ((mouse.getAbsoluteX() >= menu.getX())
-                    && (mouse.getAbsoluteX() < menu.getX()
+                if ((mouse.getAbsoluteX() >= menu.getTitleX())
+                    && (mouse.getAbsoluteX() < menu.getTitleX()
                         + menu.getTitle().length() + 2)
                 ) {
                     menu.setActive(true);
@@ -2018,8 +2127,8 @@ public class TApplication implements Runnable {
 
             // See if we should switch menus
             for (TMenu menu: menus) {
-                if ((mouse.getAbsoluteX() >= menu.getX())
-                    && (mouse.getAbsoluteX() < menu.getX()
+                if ((mouse.getAbsoluteX() >= menu.getTitleX())
+                    && (mouse.getAbsoluteX() < menu.getTitleX()
                         + menu.getTitle().length() + 2)
                 ) {
                     menu.setActive(true);
@@ -2280,7 +2389,32 @@ public class TApplication implements Runnable {
         int x = 0;
         for (TMenu menu: menus) {
             menu.setX(x);
+            menu.setTitleX(x);
             x += menu.getTitle().length() + 2;
+
+            // Don't let the menu window exceed the screen width
+            int rightEdge = menu.getX() + menu.getWidth();
+            if (rightEdge > getScreen().getWidth()) {
+                menu.setX(getScreen().getWidth() - menu.getWidth());
+            }
+        }
+    }
+
+    /**
+     * Post an event to process.
+     *
+     * @param event new event to add to the queue
+     */
+    public final void postEvent(final TInputEvent event) {
+        synchronized (this) {
+            synchronized (fillEventQueue) {
+                fillEventQueue.add(event);
+            }
+            if (debugThreads) {
+                System.err.println(System.currentTimeMillis() + " " +
+                    Thread.currentThread() + " postEvent() wake up main");
+            }
+            this.notify();
         }
     }
 
@@ -2333,14 +2467,14 @@ public class TApplication implements Runnable {
      * @return the new menu
      */
     public final TMenu addFileMenu() {
-        TMenu fileMenu = addMenu("&File");
+        TMenu fileMenu = addMenu(i18n.getString("fileMenuTitle"));
         fileMenu.addDefaultItem(TMenu.MID_OPEN_FILE);
         fileMenu.addSeparator();
         fileMenu.addDefaultItem(TMenu.MID_SHELL);
         fileMenu.addDefaultItem(TMenu.MID_EXIT);
-        TStatusBar statusBar = fileMenu.newStatusBar("File-management " +
-            "commands (Open, Save, Print, etc.)");
-        statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
+        TStatusBar statusBar = fileMenu.newStatusBar(i18n.
+            getString("fileMenuStatus"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
         return fileMenu;
     }
 
@@ -2350,14 +2484,14 @@ public class TApplication implements Runnable {
      * @return the new menu
      */
     public final TMenu addEditMenu() {
-        TMenu editMenu = addMenu("&Edit");
+        TMenu editMenu = addMenu(i18n.getString("editMenuTitle"));
         editMenu.addDefaultItem(TMenu.MID_CUT);
         editMenu.addDefaultItem(TMenu.MID_COPY);
         editMenu.addDefaultItem(TMenu.MID_PASTE);
         editMenu.addDefaultItem(TMenu.MID_CLEAR);
-        TStatusBar statusBar = editMenu.newStatusBar("Editor operations, " +
-            "undo, and Clipboard access");
-        statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
+        TStatusBar statusBar = editMenu.newStatusBar(i18n.
+            getString("editMenuStatus"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
         return editMenu;
     }
 
@@ -2367,7 +2501,7 @@ public class TApplication implements Runnable {
      * @return the new menu
      */
     public final TMenu addWindowMenu() {
-        TMenu windowMenu = addMenu("&Window");
+        TMenu windowMenu = addMenu(i18n.getString("windowMenuTitle"));
         windowMenu.addDefaultItem(TMenu.MID_TILE);
         windowMenu.addDefaultItem(TMenu.MID_CASCADE);
         windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL);
@@ -2377,9 +2511,9 @@ public class TApplication implements Runnable {
         windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
         windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
         windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
-        TStatusBar statusBar = windowMenu.newStatusBar("Open, arrange, and " +
-            "list windows");
-        statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
+        TStatusBar statusBar = windowMenu.newStatusBar(i18n.
+            getString("windowMenuStatus"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
         return windowMenu;
     }
 
@@ -2389,7 +2523,7 @@ public class TApplication implements Runnable {
      * @return the new menu
      */
     public final TMenu addHelpMenu() {
-        TMenu helpMenu = addMenu("&Help");
+        TMenu helpMenu = addMenu(i18n.getString("helpMenuTitle"));
         helpMenu.addDefaultItem(TMenu.MID_HELP_CONTENTS);
         helpMenu.addDefaultItem(TMenu.MID_HELP_INDEX);
         helpMenu.addDefaultItem(TMenu.MID_HELP_SEARCH);
@@ -2398,8 +2532,9 @@ public class TApplication implements Runnable {
         helpMenu.addDefaultItem(TMenu.MID_HELP_ACTIVE_FILE);
         helpMenu.addSeparator();
         helpMenu.addDefaultItem(TMenu.MID_ABOUT);
-        TStatusBar statusBar = helpMenu.newStatusBar("Access online help");
-        statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
+        TStatusBar statusBar = helpMenu.newStatusBar(i18n.
+            getString("helpMenuStatus"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
         return helpMenu;
     }
 
@@ -2417,7 +2552,8 @@ public class TApplication implements Runnable {
     protected boolean onCommand(final TCommandEvent command) {
         // Default: handle cmExit
         if (command.equals(cmExit)) {
-            if (messageBox("Confirmation", "Exit application?",
+            if (messageBox(i18n.getString("exitDialogTitle"),
+                    i18n.getString("exitDialogText"),
                     TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
                 exit();
             }
@@ -2466,7 +2602,8 @@ public class TApplication implements Runnable {
 
         // Default: handle MID_EXIT
         if (menu.getId() == TMenu.MID_EXIT) {
-            if (messageBox("Confirmation", "Exit application?",
+            if (messageBox(i18n.getString("exitDialogTitle"),
+                    i18n.getString("exitDialogText"),
                     TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
                 exit();
             }
@@ -2494,6 +2631,10 @@ public class TApplication implements Runnable {
             showAboutDialog();
             return true;
         }
+        if (menu.getId() == TMenu.MID_REPAINT) {
+            doRepaint();
+            return true;
+        }
         return false;
     }
 
@@ -2680,6 +2821,37 @@ public class TApplication implements Runnable {
         return new TTerminalWindow(this, x, y, flags);
     }
 
+    /**
+     * Convenience function to open a terminal window and execute a custom
+     * command line inside it.
+     *
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param commandLine the command line to execute
+     * @return the terminal new window
+     */
+    public final TTerminalWindow openTerminal(final int x, final int y,
+        final String commandLine) {
+
+        return openTerminal(x, y, TWindow.RESIZABLE, commandLine);
+    }
+
+    /**
+     * Convenience function to open a terminal window and execute a custom
+     * command line inside it.
+     *
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param flags mask of CENTERED, MODAL, or RESIZABLE
+     * @param commandLine the command line to execute
+     * @return the terminal new window
+     */
+    public final TTerminalWindow openTerminal(final int x, final int y,
+        final int flags, final String commandLine) {
+
+        return new TTerminalWindow(this, x, y, flags, commandLine);
+    }
+
     /**
      * Convenience function to spawn an file open box.
      *
@@ -2722,6 +2894,7 @@ public class TApplication implements Runnable {
         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).
@@ -2772,4 +2945,17 @@ public class TApplication implements Runnable {
         return window;
     }
 
+    /**
+     * Convenience function to open a file in an editor window and make it
+     * active.
+     *
+     * @param file the file to open
+     * @throws IOException if a java.io operation throws
+     */
+    public final TEditorWindow addEditor(final File file) throws IOException {
+
+        TEditorWindow editor = new TEditorWindow(this, file);
+        return editor;
+    }
+
 }