Expose width/height in TApplication constructor, attempt on ECMA48
[fanfix.git] / src / jexer / TApplication.java
index cedb631f77fa40188112ac5caa04e2dd67756733..e61cea28d631afba3c84964c7a2a13803c597aca 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);
     }
 
@@ -546,6 +575,39 @@ public class TApplication implements Runnable {
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Public constructor.
+     *
+     * @param backendType BackendType.XTERM, BackendType.ECMA48 or
+     * BackendType.SWING
+     * @param windowWidth the number of text columns to start with
+     * @param windowHeight the number of text rows to start with
+     * @param fontSize the size in points
+     * @throws UnsupportedEncodingException if an exception is thrown when
+     * creating the InputStreamReader
+     */
+    public TApplication(final BackendType backendType, final int windowWidth,
+        final int windowHeight, final int fontSize)
+        throws UnsupportedEncodingException {
+
+        switch (backendType) {
+        case SWING:
+            backend = new SwingBackend(this, windowWidth, windowHeight,
+                fontSize);
+            break;
+        case XTERM:
+            // Fall through...
+        case ECMA48:
+            backend = new ECMA48Backend(this, null, null, windowWidth,
+                windowHeight, fontSize);
+            break;
+        default:
+            throw new IllegalArgumentException("Invalid backend type: "
+                + backendType);
+        }
+        TApplicationImpl();
+    }
+
     /**
      * Public constructor.
      *
@@ -732,6 +794,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 +863,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 +916,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 +973,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 +1099,10 @@ 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;
@@ -1053,6 +1126,7 @@ public class TApplication implements Runnable {
         if (debugEvents) {
             System.err.printf("Handle event: %s\n", event);
         }
+        TMouseEvent doubleClick = null;
 
         // Special application-wide events -----------------------------------
 
@@ -1064,6 +1138,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
@@ -1171,6 +1264,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;
                 }
@@ -1183,11 +1281,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);
+                }
             }
         }
     }
@@ -1201,6 +1305,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;
@@ -1209,10 +1315,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);
+        }
     }
 
     /**
@@ -1413,9 +1541,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);
@@ -1522,6 +1656,12 @@ public class TApplication implements Runnable {
             windows.remove(0);
             activeWindow = null;
             for (TWindow w: windows) {
+
+                // Do not activate a hidden window.
+                if (w.isHidden()) {
+                    continue;
+                }
+
                 if (w.getZ() > z) {
                     w.setZ(w.getZ() - 1);
                     if (w.getZ() == 0) {
@@ -1840,10 +1980,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;
                     }
@@ -1992,8 +2138,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);
@@ -2020,8 +2166,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);
@@ -2282,7 +2428,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();
         }
     }
 
@@ -2335,14 +2506,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;
     }
 
@@ -2352,14 +2523,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;
     }
 
@@ -2369,7 +2540,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);
@@ -2379,9 +2550,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;
     }
 
@@ -2391,7 +2562,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);
@@ -2400,8 +2571,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;
     }
 
@@ -2419,7 +2591,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();
             }
@@ -2468,7 +2641,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();
             }
@@ -2496,6 +2670,10 @@ public class TApplication implements Runnable {
             showAboutDialog();
             return true;
         }
+        if (menu.getId() == TMenu.MID_REPAINT) {
+            doRepaint();
+            return true;
+        }
         return false;
     }
 
@@ -2682,6 +2860,53 @@ 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 command 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 [] command) {
+
+        return new TTerminalWindow(this, x, y, flags, command);
+    }
+
+    /**
+     * 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.split("\\s"));
+    }
+
     /**
      * Convenience function to spawn an file open box.
      *
@@ -2724,6 +2949,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).
@@ -2774,4 +3000,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;
+    }
+
 }