Expose width/height in TApplication constructor, attempt on ECMA48
[nikiroo-utils.git] / src / jexer / TApplication.java
index 66f711017604bfb1f37b89fb5ad91612ec4eee78..e61cea28d631afba3c84964c7a2a13803c597aca 100644 (file)
@@ -28,6 +28,7 @@
  */
 package jexer;
 
+import java.io.File;
 import java.io.InputStream;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -282,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();
@@ -349,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().
      */
@@ -414,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.
      */
@@ -554,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.
      *
@@ -919,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;
@@ -1043,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;
@@ -1066,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 -----------------------------------
 
@@ -1077,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
@@ -1184,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;
                 }
@@ -1196,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);
+                }
             }
         }
     }
@@ -1214,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;
@@ -1222,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);
+        }
     }
 
     /**
@@ -1426,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);
@@ -1535,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) {
@@ -1853,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;
                     }
@@ -2005,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);
@@ -2033,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);
@@ -2295,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();
         }
     }
 
@@ -2702,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.
      *
@@ -2744,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).
@@ -2794,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;
+    }
+
 }