Bug fixes
authorKevin Lamonte <kevin.lamonte@gmail.com>
Wed, 15 Mar 2017 22:12:56 +0000 (18:12 -0400)
committerKevin Lamonte <kevin.lamonte@gmail.com>
Wed, 15 Mar 2017 22:12:56 +0000 (18:12 -0400)
12 files changed:
README.md
build.xml
src/jexer/TApplication.java
src/jexer/TTerminalWindow.java
src/jexer/TText.java
src/jexer/TWindow.java
src/jexer/io/ECMA48Screen.java
src/jexer/io/ReadTimeoutException.java [new file with mode: 0644]
src/jexer/io/TimeoutInputStream.java [new file with mode: 0644]
src/jexer/menu/TMenu.java
src/jexer/menu/TMenuItem.java
src/jexer/tterminal/ECMA48.java

index 4e71a9c60490da52f21588553934f9d7d2b1340f..ab6069ea253144c30aed456e26f10b5fda6c1b8e 100644 (file)
--- a/README.md
+++ b/README.md
@@ -172,12 +172,6 @@ Some arbitrary design decisions had to be made when either the
 obviously expected behavior did not happen or when a specification was
 ambiguous.  This section describes such issues.
 
-  - TTerminalWindow will hang on input from the remote if the
-    TApplication is exited before the TTerminalWindow's process has
-    closed on its own.  This is due to a Java limitation/interaction
-    between blocking reads (which is necessary to get UTF8 translation
-    correct) and file streams.
-
   - See jexer.tterminal.ECMA48 for more specifics of terminal
     emulation limitations.
 
@@ -191,6 +185,9 @@ ambiguous.  This section describes such issues.
     checking for a tty: script launches $SHELL in a pseudo-tty.  This
     works on Linux but might not on other Posix-y platforms.
 
+  - Closing a TTerminalWindow without exiting the process inside it
+    may result in a zombie 'script' process.
+
   - Java's InputStreamReader as used by the ECMA48 backend requires a
     valid UTF-8 stream.  The default X10 encoding for mouse
     coordinates outside (160,94) can corrupt that stream, at best
index 0609921d2372770fead3194d8a15313e3c29f971..45014d9566523d1ab5735279e220fb8b26685bd5 100644 (file)
--- a/build.xml
+++ b/build.xml
@@ -30,7 +30,7 @@
 
 <project name="jexer" basedir="." default="jar">
 
-  <property name="build.compiler" value="gcj"/>
+  <!-- <property name="build.compiler" value="gcj"/> -->
 
   <property name="src.dir"       value="src"/>
   <property name="resources.dir" value="resources"/>
index 0a2ab19548f7a1706b3dec7656803bd7c478fb57..971f6f8fa572ff243645975966243dc9ff25f598 100644 (file)
@@ -965,20 +965,35 @@ public class TApplication implements Runnable {
         if (event instanceof TKeypressEvent) {
             TKeypressEvent keypress = (TKeypressEvent) event;
 
-            // See if this key matches an accelerator, and if so dispatch the
-            // menu event.
-            TKeypress keypressLowercase = keypress.getKey().toLowerCase();
-            TMenuItem item = null;
-            synchronized (accelerators) {
-                item = accelerators.get(keypressLowercase);
+            // See if this key matches an accelerator, and is not being
+            // 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 (item != null) {
-                if (item.isEnabled()) {
-                    // Let the menu item dispatch
-                    item.dispatch();
-                    return;
+
+            if (!windowWillShortcut) {
+                TKeypress keypressLowercase = keypress.getKey().toLowerCase();
+                TMenuItem item = null;
+                synchronized (accelerators) {
+                    item = accelerators.get(keypressLowercase);
+                }
+                if (item != null) {
+                    if (item.isEnabled()) {
+                        // Let the menu item dispatch
+                        item.dispatch();
+                        return;
+                    }
                 }
             }
+
             // Handle the keypress
             if (onKeypress(keypress)) {
                 return;
@@ -1659,7 +1674,7 @@ public class TApplication implements Runnable {
      *
      * @param event new event to add to the queue
      */
-    public final void addMenuEvent(final TInputEvent event) {
+    public final void postMenuEvent(final TInputEvent event) {
         synchronized (fillEventQueue) {
             fillEventQueue.add(event);
         }
@@ -1745,11 +1760,8 @@ public class TApplication implements Runnable {
         if (activeMenu != null) {
             return;
         }
-
-        synchronized (windows) {
-            for (TWindow window: windows) {
-                closeWindow(window);
-            }
+        while (windows.size() > 0) {
+            closeWindow(windows.get(0));
         }
     }
 
index 8730dfe7a8f98eadcc4df7af8ed9a76f89d7b17c..6d97531aabbdb76e4d896127d284c3766a7e16f5 100644 (file)
@@ -65,6 +65,76 @@ public class TTerminalWindow extends TWindow {
      */
     private TVScroller vScroller;
 
+    /**
+     * Claim the keystrokes the emulator will need.
+     */
+    private void addShortcutKeys() {
+        addShortcutKeypress(kbCtrlA);
+        addShortcutKeypress(kbCtrlB);
+        addShortcutKeypress(kbCtrlC);
+        addShortcutKeypress(kbCtrlD);
+        addShortcutKeypress(kbCtrlE);
+        addShortcutKeypress(kbCtrlF);
+        addShortcutKeypress(kbCtrlG);
+        addShortcutKeypress(kbCtrlH);
+        addShortcutKeypress(kbCtrlU);
+        addShortcutKeypress(kbCtrlJ);
+        addShortcutKeypress(kbCtrlK);
+        addShortcutKeypress(kbCtrlL);
+        addShortcutKeypress(kbCtrlM);
+        addShortcutKeypress(kbCtrlN);
+        addShortcutKeypress(kbCtrlO);
+        addShortcutKeypress(kbCtrlP);
+        addShortcutKeypress(kbCtrlQ);
+        addShortcutKeypress(kbCtrlR);
+        addShortcutKeypress(kbCtrlS);
+        addShortcutKeypress(kbCtrlT);
+        addShortcutKeypress(kbCtrlU);
+        addShortcutKeypress(kbCtrlV);
+        addShortcutKeypress(kbCtrlW);
+        addShortcutKeypress(kbCtrlX);
+        addShortcutKeypress(kbCtrlY);
+        addShortcutKeypress(kbCtrlZ);
+        addShortcutKeypress(kbF1);
+        addShortcutKeypress(kbF2);
+        addShortcutKeypress(kbF3);
+        addShortcutKeypress(kbF4);
+        addShortcutKeypress(kbF5);
+        addShortcutKeypress(kbF6);
+        addShortcutKeypress(kbF7);
+        addShortcutKeypress(kbF8);
+        addShortcutKeypress(kbF9);
+        addShortcutKeypress(kbF10);
+        addShortcutKeypress(kbF11);
+        addShortcutKeypress(kbF12);
+        addShortcutKeypress(kbAltA);
+        addShortcutKeypress(kbAltB);
+        addShortcutKeypress(kbAltC);
+        addShortcutKeypress(kbAltD);
+        addShortcutKeypress(kbAltE);
+        addShortcutKeypress(kbAltF);
+        addShortcutKeypress(kbAltG);
+        addShortcutKeypress(kbAltH);
+        addShortcutKeypress(kbAltU);
+        addShortcutKeypress(kbAltJ);
+        addShortcutKeypress(kbAltK);
+        addShortcutKeypress(kbAltL);
+        addShortcutKeypress(kbAltM);
+        addShortcutKeypress(kbAltN);
+        addShortcutKeypress(kbAltO);
+        addShortcutKeypress(kbAltP);
+        addShortcutKeypress(kbAltQ);
+        addShortcutKeypress(kbAltR);
+        addShortcutKeypress(kbAltS);
+        addShortcutKeypress(kbAltT);
+        addShortcutKeypress(kbAltU);
+        addShortcutKeypress(kbAltV);
+        addShortcutKeypress(kbAltW);
+        addShortcutKeypress(kbAltX);
+        addShortcutKeypress(kbAltY);
+        addShortcutKeypress(kbAltZ);
+    }
+
     /**
      * Public constructor spawns a shell.
      *
@@ -116,6 +186,9 @@ public class TTerminalWindow extends TWindow {
         // Setup the scroll bars
         onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
                 getHeight()));
+
+        // Claim the keystrokes the emulator will need.
+        addShortcutKeys();
     }
 
     /**
@@ -144,6 +217,8 @@ public class TTerminalWindow extends TWindow {
         onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
                 getHeight()));
 
+        // Claim the keystrokes the emulator will need.
+        addShortcutKeys();
     }
 
     /**
@@ -240,11 +315,11 @@ public class TTerminalWindow extends TWindow {
      * Handle window close.
      */
     @Override public void onClose() {
+        emulator.close();
         if (shell != null) {
+            // System.err.println("shell.destroy()");
             shell.destroy();
             shell = null;
-        } else {
-            emulator.close();
         }
     }
 
@@ -286,6 +361,7 @@ public class TTerminalWindow extends TWindow {
                             getTitle(), rc));
                     shell = null;
                     emulator.close();
+                    clearShortcutKeypresses();
                 } catch (IllegalThreadStateException e) {
                     // The emulator thread has exited, but the shell Process
                     // hasn't figured that out yet.  Do nothing, we will see
@@ -300,6 +376,7 @@ public class TTerminalWindow extends TWindow {
                             getTitle(), rc));
                     shell = null;
                     emulator.close();
+                    clearShortcutKeypresses();
                 } catch (IllegalThreadStateException e) {
                     // The shell is still running, do nothing.
                 }
index 1750be113f636fe0385e885fe525fe04f095d361..007ff2d5a7d95fbe0ee1ab95cedd4a8422dcc8a3 100644 (file)
@@ -88,8 +88,7 @@ public final class TText extends TWidget {
     /**
      * Convenience method used by TWindowLoggerOutput.
      *
-     * @param line
-     *            new line to add
+     * @param line new line to add
      */
     public void addLine(final String line) {
         if (text.length() == 0) {
@@ -134,10 +133,8 @@ public final class TText extends TWidget {
      * the final string with a newline. Note that interior newlines are
      * converted to spaces.
      *
-     * @param str
-     *            the string
-     * @param n
-     *            the maximum number of characters in a line
+     * @param str the string
+     * @param n the maximum number of characters in a line
      * @return the wrapped string
      */
     private String wrap(final String str, final int n) {
@@ -221,18 +218,12 @@ public final class TText extends TWidget {
     /**
      * Public constructor.
      *
-     * @param parent
-     *            parent widget
-     * @param text
-     *            text on the screen
-     * @param x
-     *            column relative to parent
-     * @param y
-     *            row relative to parent
-     * @param width
-     *            width of text area
-     * @param height
-     *            height of text area
+     * @param parent parent widget
+     * @param text text on the screen
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of text area
+     * @param height height of text area
      */
     public TText(final TWidget parent, final String text, final int x,
             final int y, final int width, final int height) {
@@ -243,21 +234,14 @@ public final class TText extends TWidget {
     /**
      * Public constructor.
      *
-     * @param parent
-     *            parent widget
-     * @param text
-     *            text on the screen
-     * @param x
-     *            column relative to parent
-     * @param y
-     *            row relative to parent
-     * @param width
-     *            width of text area
-     * @param height
-     *            height of text area
-     * @param colorKey
-     *            ColorTheme key color to use for foreground text. Default is
-     *            "ttext"
+     * @param parent parent widget
+     * @param text text on the screen
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of text area
+     * @param height height of text area
+     * @param colorKey ColorTheme key color to use for foreground
+     * text. Default is "ttext".
      */
     public TText(final TWidget parent, final String text, final int x,
             final int y, final int width, final int height,
@@ -311,8 +295,7 @@ public final class TText extends TWidget {
     /**
      * Handle mouse press events.
      *
-     * @param mouse
-     *            mouse button press event
+     * @param mouse mouse button press event
      */
     @Override
     public void onMouseDown(final TMouseEvent mouse) {
@@ -332,8 +315,7 @@ public final class TText extends TWidget {
     /**
      * Handle keystrokes.
      *
-     * @param keypress
-     *            keystroke event
+     * @param keypress keystroke event
      */
     @Override
     public void onKeypress(final TKeypressEvent keypress) {
index 26bd8c453a1922bfd8f79b47c61b35f1470ae429..73026953eaa2aa5ffaa5985f6489d9fb1170e2e8 100644 (file)
@@ -28,6 +28,8 @@
  */
 package jexer;
 
+import java.util.HashSet;
+
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
 import jexer.bits.GraphicsChars;
@@ -137,6 +139,48 @@ public class TWindow extends TWidget {
         this.z = z;
     }
 
+    /**
+     * Window's keyboard shortcuts.  Any key in this set will be passed to
+     * the window directly rather than processed through the menu
+     * accelerators.
+     */
+    private HashSet<TKeypress> keyboardShortcuts = new HashSet<TKeypress>();
+
+    /**
+     * Add a keypress to be overridden for this window.
+     *
+     * @param key the key to start taking control of
+     */
+    protected void addShortcutKeypress(final TKeypress key) {
+        keyboardShortcuts.add(key);
+    }
+
+    /**
+     * Remove a keypress to be overridden for this window.
+     *
+     * @param key the key to stop taking control of
+     */
+    protected void removeShortcutKeypress(final TKeypress key) {
+        keyboardShortcuts.remove(key);
+    }
+
+    /**
+     * Remove all keypresses to be overridden for this window.
+     */
+    protected void clearShortcutKeypresses() {
+        keyboardShortcuts.clear();
+    }
+
+    /**
+     * Determine if a keypress is overridden for this window.
+     *
+     * @param key the key to check
+     * @return true if this window wants to process this key on its own
+     */
+    public boolean isShortcutKeypress(final TKeypress key) {
+        return keyboardShortcuts.contains(key);
+    }
+
     /**
      * If true, then the user clicked on the title bar and is moving the
      * window.
index 4c3af185db87b929a26aa2447406d75c0000d921..a00a1806e644b31857d58feddf14fdb0a5deffb7 100644 (file)
@@ -118,7 +118,7 @@ public final class ECMA48Screen extends Screen {
 
                     for (int i = x; i < width; i++) {
                         assert (logical[i][y].isBlank());
-                        // Physical is always updatesd
+                        // Physical is always updated
                         physical[i][y].reset();
                     }
 
diff --git a/src/jexer/io/ReadTimeoutException.java b/src/jexer/io/ReadTimeoutException.java
new file mode 100644 (file)
index 0000000..0a1bbc5
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * 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"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.io;
+
+import java.io.IOException;
+
+/**
+ * ReadTimeoutException is thrown by TimeoutInputStream.read() when bytes are
+ * not available within the timeout specified.
+ */
+public class ReadTimeoutException extends IOException {
+
+    /**
+     * Construct an instance with a message.
+     *
+     * @param msg exception text
+     */
+    public ReadTimeoutException(String msg) {
+        super(msg);
+    }
+}
diff --git a/src/jexer/io/TimeoutInputStream.java b/src/jexer/io/TimeoutInputStream.java
new file mode 100644 (file)
index 0000000..f1b140b
--- /dev/null
@@ -0,0 +1,365 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * 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"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * This class provides an optional millisecond timeout on its read()
+ * operations.  This permits callers to bail out rather than block.
+ */
+public class TimeoutInputStream extends InputStream {
+
+    /**
+     * The wrapped stream.
+     */
+    private InputStream stream;
+
+    /**
+     * The timeout value in millis.  If it takes longer than this for bytes
+     * to be available for read then a ReadTimeoutException is thrown.  A
+     * value of 0 means to block as a normal InputStream would.
+     */
+    private int timeoutMillis;
+
+    /**
+     * If true, the current read() will timeout soon.
+     */
+    private volatile boolean cancel = false;
+
+    /**
+     * Request that the current read() operation timeout immediately.
+     */
+    public synchronized void cancelRead() {
+        cancel = true;
+    }
+
+    /**
+     * Public constructor, at the default timeout of 10000 millis (10
+     * seconds).
+     *
+     * @param stream the wrapped InputStream
+     */
+    public TimeoutInputStream(final InputStream stream) {
+        this.stream             = stream;
+        this.timeoutMillis      = 10000;
+    }
+
+    /**
+     * Public constructor, using the default 10 bits per byte.
+     *
+     * @param stream the wrapped InputStream
+     * @param timeoutMillis the timeout value in millis.  If it takes longer
+     * than this for bytes to be available for read then a
+     * ReadTimeoutException is thrown.  A value of 0 means to block as a
+     * normal InputStream would.
+     */
+    public TimeoutInputStream(final InputStream stream,
+        final int timeoutMillis) {
+
+        if (timeoutMillis < 0) {
+            throw new IllegalArgumentException("Invalid timeoutMillis value, " +
+                "must be >= 0");
+        }
+
+        this.stream             = stream;
+        this.timeoutMillis      = timeoutMillis;
+    }
+
+    /**
+     * Reads the next byte of data from the input stream.
+     *
+     * @return the next byte of data, or -1 if there is no more data because
+     * the end of the stream has been reached.
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public int read() throws IOException {
+
+        if (timeoutMillis == 0) {
+            // Block on the read().
+            return stream.read();
+        }
+
+        if (stream.available() > 0) {
+            // A byte is available now, return it.
+            return stream.read();
+        }
+
+        // We will wait up to timeoutMillis to see if a byte is available.
+        // If not, we throw ReadTimeoutException.
+        long checkTime = System.currentTimeMillis();
+        while (stream.available() == 0) {
+            long now = System.currentTimeMillis();
+            synchronized (this) {
+                if ((now - checkTime > timeoutMillis) || (cancel == true)) {
+                    if (cancel == true) {
+                        cancel = false;
+                    }
+                    throw new ReadTimeoutException("Timeout on read(): " +
+                        (int) (now - checkTime) + " millis and still no data");
+                }
+            }
+            try {
+                // How long do we sleep for, eh?  For now we will go with 2
+                // millis.
+                Thread.currentThread().sleep(2);
+            } catch (InterruptedException e) {
+                // SQUASH
+            }
+        }
+
+        if (stream.available() > 0) {
+            // A byte is available now, return it.
+            return stream.read();
+        }
+
+        throw new IOException("InputStream claimed a byte was available, but " +
+            "now it is not.  What is going on?");
+    }
+
+    /**
+     * Reads some number of bytes from the input stream and stores them into
+     * the buffer array b.
+     *
+     * @param b the buffer into which the data is read.
+     * @return the total number of bytes read into the buffer, or -1 if there
+     * is no more data because the end of the stream has been reached.
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public int read(final byte[] b) throws IOException {
+        if (timeoutMillis == 0) {
+            // Block on the read().
+            return stream.read(b);
+        }
+
+        int remaining = b.length;
+
+        if (stream.available() >= remaining) {
+            // Enough bytes are available now, return them.
+            return stream.read(b);
+        }
+
+        while (remaining > 0) {
+
+            // We will wait up to timeoutMillis to see if a byte is
+            // available.  If not, we throw ReadTimeoutException.
+            long checkTime = System.currentTimeMillis();
+            while (stream.available() == 0) {
+                long now = System.currentTimeMillis();
+
+                synchronized (this) {
+                    if ((now - checkTime > timeoutMillis) || (cancel == true)) {
+                        if (cancel == true) {
+                            cancel = false;
+                        }
+                        throw new ReadTimeoutException("Timeout on read(): " +
+                            (int) (now - checkTime) + " millis and still no " +
+                            "data");
+                    }
+                }
+                try {
+                    // How long do we sleep for, eh?  For now we will go with
+                    // 2 millis.
+                    Thread.currentThread().sleep(2);
+                } catch (InterruptedException e) {
+                    // SQUASH
+                }
+            }
+
+            if (stream.available() > 0) {
+                // At least one byte is available now, read it.
+                int n = stream.available();
+                if (remaining < n) {
+                    n = remaining;
+                }
+                int rc = stream.read(b, b.length - remaining, n);
+                if (rc == -1) {
+                    // This shouldn't happen.
+                    throw new IOException("InputStream claimed bytes were " +
+                        "available, but read() returned -1.  What is going " +
+                        "on?");
+                }
+                remaining -= rc;
+                return rc;
+            }
+        }
+
+        throw new IOException("InputStream claimed all bytes were available, " +
+            "but now it is not.  What is going on?");
+    }
+
+    /**
+     * Reads up to len bytes of data from the input stream into an array of
+     * bytes.
+     *
+     * @param b the buffer into which the data is read.
+     * @param off the start offset in array b at which the data is written.
+     * @param len the maximum number of bytes to read.
+     * @return the total number of bytes read into the buffer, or -1 if there
+     * is no more data because the end of the stream has been reached.
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public int read(final byte[] b, final int off,
+        final int len) throws IOException {
+
+        if (timeoutMillis == 0) {
+            // Block on the read().
+            return stream.read(b);
+        }
+
+        int remaining = len;
+
+        if (stream.available() >= remaining) {
+            // Enough bytes are available now, return them.
+            return stream.read(b, off, remaining);
+        }
+
+        while (remaining > 0) {
+
+            // We will wait up to timeoutMillis to see if a byte is
+            // available.  If not, we throw ReadTimeoutException.
+            long checkTime = System.currentTimeMillis();
+            while (stream.available() == 0) {
+                long now = System.currentTimeMillis();
+                synchronized (this) {
+                    if ((now - checkTime > timeoutMillis) || (cancel == true)) {
+                        if (cancel == true) {
+                            cancel = false;
+                        }
+                        throw new ReadTimeoutException("Timeout on read(): " +
+                            (int) (now - checkTime) + " millis and still no " +
+                            "data");
+                    }
+                }
+                try {
+                    // How long do we sleep for, eh?  For now we will go with
+                    // 2 millis.
+                    Thread.currentThread().sleep(2);
+                } catch (InterruptedException e) {
+                    // SQUASH
+                }
+            }
+
+            if (stream.available() > 0) {
+                // At least one byte is available now, read it.
+                int n = stream.available();
+                if (remaining < n) {
+                    n = remaining;
+                }
+                int rc = stream.read(b, off + len - remaining, n);
+                if (rc == -1) {
+                    // This shouldn't happen.
+                    throw new IOException("InputStream claimed bytes were " +
+                        "available, but read() returned -1.  What is going " +
+                        "on?");
+                }
+                return rc;
+            }
+        }
+
+        throw new IOException("InputStream claimed all bytes were available, " +
+            "but now it is not.  What is going on?");
+    }
+
+    /**
+     * Returns an estimate of the number of bytes that can be read (or
+     * skipped over) from this input stream without blocking by the next
+     * invocation of a method for this input stream.
+     *
+     * @return an estimate of the number of bytes that can be read (or
+     * skipped over) from this input stream without blocking or 0 when it
+     * reaches the end of the input stream.
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public int available() throws IOException {
+        return stream.available();
+    }
+
+    /**
+     * Closes this input stream and releases any system resources associated
+     * with the stream.
+     *
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public void close() throws IOException {
+        stream.close();
+    }
+
+    /**
+     * Marks the current position in this input stream.
+     *
+     * @param readLimit the maximum limit of bytes that can be read before
+     * the mark position becomes invalid
+     */
+    @Override
+    public void mark(final int readLimit) {
+        stream.mark(readLimit);
+    }
+
+    /**
+     * Tests if this input stream supports the mark and reset methods.
+     *
+     * @return true if this stream instance supports the mark and reset
+     * methods; false otherwise
+     */
+    @Override
+    public boolean markSupported() {
+        return stream.markSupported();
+    }
+
+    /**
+     * Repositions this stream to the position at the time the mark method
+     * was last called on this input stream.
+     *
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public void reset() throws IOException {
+        stream.reset();
+    }
+
+    /**
+     * Skips over and discards n bytes of data from this input stream.
+     *
+     * @param n the number of bytes to be skipped
+     * @return the actual number of bytes skipped
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public long skip(final long n) throws IOException {
+        return stream.skip(n);
+    }
+
+}
index 8915c0c290a93eaf52325e4130d78eb9f9a21bec..b0f99ae257e2de7a5890db1eb5dc71593413b5c5 100644 (file)
@@ -436,7 +436,7 @@ public final class TMenu extends TWindow {
             break;
         case MID_WINDOW_CLOSE:
             label = "&Close";
-            // key = kbCtrlW;
+            key = kbCtrlW;
             break;
 
         default:
index ec2afb232718ca8d4c565e114a20b37a3d4d95c3..fe74c4e7f8b837dfc361c4a697b0e0610e470ab0 100644 (file)
@@ -255,7 +255,7 @@ public class TMenuItem extends TWidget {
     public void dispatch() {
         assert (isEnabled());
 
-        getApplication().addMenuEvent(new TMenuEvent(id));
+        getApplication().postMenuEvent(new TMenuEvent(id));
         if (checkable) {
             checked = !checked;
         }
index f657eaffa1255bdf0ee5a391ca0cfcdaaec0aa0f..b3cb0ac556aded9743240714e087d738988d638c 100644 (file)
@@ -46,6 +46,8 @@ import jexer.event.TMouseEvent;
 import jexer.bits.Color;
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
+import jexer.io.ReadTimeoutException;
+import jexer.io.TimeoutInputStream;
 import static jexer.TKeypress.*;
 
 /**
@@ -250,75 +252,53 @@ public class ECMA48 implements Runnable {
      */
     public final void close() {
 
-        // Synchronize so we don't stomp on the reader thread.
-        synchronized (this) {
-
-            // Close the input stream
-            switch (type) {
-            case VT100:
-            case VT102:
-            case VT220:
-                if (inputStream != null) {
-                    try {
-                        inputStream.close();
-                    } catch (IOException e) {
-                        // SQUASH
-                    }
-                    inputStream = null;
-                }
-                break;
-            case XTERM:
-                if (input != null) {
-                    try {
-                        input.close();
-                    } catch (IOException e) {
-                        // SQUASH
-                    }
-                    input = null;
-                    inputStream = null;
-                }
-                break;
+        // Tell the reader thread to stop looking at input.  It will close
+        // the input streams.
+        if (stopReaderThread == false) {
+            stopReaderThread = true;
+            try {
+                readerThread.join(1000);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
             }
+        }
 
-            // Tell the reader thread to stop looking at input.
-            if (stopReaderThread == false) {
-                stopReaderThread = true;
+        // Now close the output stream.
+        switch (type) {
+        case VT100:
+        case VT102:
+        case VT220:
+            if (outputStream != null) {
                 try {
-                    readerThread.join();
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
+                    outputStream.close();
+                } catch (IOException e) {
+                    // SQUASH
                 }
+                outputStream = null;
             }
-
-            // Close the output stream.
-            switch (type) {
-            case VT100:
-            case VT102:
-            case VT220:
-                if (outputStream != null) {
-                    try {
-                        outputStream.close();
-                    } catch (IOException e) {
-                        // SQUASH
-                    }
-                    outputStream = null;
+            break;
+        case XTERM:
+            if (outputStream != null) {
+                try {
+                    outputStream.close();
+                } catch (IOException e) {
+                    // SQUASH
                 }
-                break;
-            case XTERM:
-                if (output != null) {
-                    try {
-                        output.close();
-                    } catch (IOException e) {
-                        // SQUASH
-                    }
-                    output = null;
+                outputStream = null;
+            }
+            if (output != null) {
+                try {
+                    output.close();
+                } catch (IOException e) {
+                    // SQUASH
                 }
-                break;
-            default:
-                throw new IllegalArgumentException("Invalid device type: "
-                    + type);
+                output = null;
             }
-        } // synchronized (this)
+            break;
+        default:
+            throw new IllegalArgumentException("Invalid device type: " +
+                type);
+        }
     }
 
     /**
@@ -393,7 +373,7 @@ public class ECMA48 implements Runnable {
     /**
      * The terminal's raw InputStream.  This is used for type != XTERM.
      */
-    private volatile InputStream inputStream;
+    private volatile TimeoutInputStream inputStream;
 
     /**
      * The terminal's output.  For type == XTERM, this wraps an
@@ -918,9 +898,13 @@ public class ECMA48 implements Runnable {
         display           = new LinkedList<DisplayLine>();
 
         this.type         = type;
-        this.inputStream  = inputStream;
+        if (inputStream instanceof TimeoutInputStream) {
+            this.inputStream  = (TimeoutInputStream)inputStream;
+        } else {
+            this.inputStream  = new TimeoutInputStream(inputStream, 2000);
+        }
         if (type == DeviceType.XTERM) {
-            this.input    = new InputStreamReader(inputStream, "UTF-8");
+            this.input    = new InputStreamReader(this.inputStream, "UTF-8");
             this.output   = new OutputStreamWriter(outputStream, "UTF-8");
             this.outputStream = null;
         } else {
@@ -6025,11 +6009,13 @@ public class ECMA48 implements Runnable {
         while (!done && !stopReaderThread) {
             try {
                 int n = inputStream.available();
+
                 // System.err.printf("available() %d\n", n); System.err.flush();
                 if (utf8) {
                     if (readBufferUTF8.length < n) {
                         // The buffer wasn't big enough, make it huger
-                        int newSizeHalf = Math.max(readBufferUTF8.length, n);
+                        int newSizeHalf = Math.max(readBufferUTF8.length,
+                            n);
 
                         readBufferUTF8 = new char[newSizeHalf * 2];
                     }
@@ -6040,15 +6026,28 @@ public class ECMA48 implements Runnable {
                         readBuffer = new byte[newSizeHalf * 2];
                     }
                 }
+                if (n == 0) {
+                    try {
+                        Thread.sleep(2);
+                    } catch (InterruptedException e) {
+                        // SQUASH
+                    }
+                    continue;
+                }
 
                 int rc = -1;
-                if (utf8) {
-                    rc = input.read(readBufferUTF8, 0,
-                        readBufferUTF8.length);
-                } else {
-                    rc = inputStream.read(readBuffer, 0,
-                        readBuffer.length);
+                try {
+                    if (utf8) {
+                        rc = input.read(readBufferUTF8, 0,
+                            readBufferUTF8.length);
+                    } else {
+                        rc = inputStream.read(readBuffer, 0,
+                            readBuffer.length);
+                    }
+                } catch (ReadTimeoutException e) {
+                    rc = 0;
                 }
+
                 // System.err.printf("read() %d\n", rc); System.err.flush();
                 if (rc == -1) {
                     // This is EOF
@@ -6061,8 +6060,9 @@ public class ECMA48 implements Runnable {
                         } else {
                             ch = readBuffer[i];
                         }
-                        // Don't step on UI events
+
                         synchronized (this) {
+                            // Don't step on UI events
                             consume((char)ch);
                         }
                     }
@@ -6072,11 +6072,26 @@ public class ECMA48 implements Runnable {
                 e.printStackTrace();
                 done = true;
             }
+
         } // while ((done == false) && (stopReaderThread == false))
 
         // Let the rest of the world know that I am done.
         stopReaderThread = true;
 
+        try {
+            inputStream.cancelRead();
+            inputStream.close();
+            inputStream = null;
+        } catch (IOException e) {
+            // SQUASH
+        }
+        try {
+            input.close();
+            input = null;
+        } catch (IOException e) {
+            // SQUASH
+        }
+
         // System.err.println("*** run() exiting..."); System.err.flush();
     }