From 92554d64c21c6a477fd23a06ca3a64a542b622a3 Mon Sep 17 00:00:00 2001 From: Kevin Lamonte Date: Thu, 19 Mar 2015 22:51:44 -0400 Subject: [PATCH] clean up threads and timers --- README.md | 15 +- src/jexer/TApplication.java | 210 +++++++++++++++++++-------- src/jexer/TButton.java | 9 ++ src/jexer/TTerminalWindow.java | 11 +- src/jexer/TTimer.java | 12 +- src/jexer/TWidget.java | 46 +++--- src/jexer/TWindow.java | 3 - src/jexer/backend/AWTBackend.java | 41 ++---- src/jexer/backend/Backend.java | 4 +- src/jexer/backend/ECMA48Backend.java | 29 +--- src/jexer/demos/Demo1.java | 3 +- src/jexer/io/AWTTerminal.java | 70 ++++----- src/jexer/io/ECMA48Terminal.java | 36 +++-- src/jexer/io/Screen.java | 10 ++ src/jexer/menu/TMenu.java | 4 - 15 files changed, 274 insertions(+), 229 deletions(-) diff --git a/README.md b/README.md index c2946c6..8d8ea20 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,15 @@ See the file demos/Demo1.java for detailed examples. +Known Issues / Arbitrary Decisions +---------------------------------- + +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. + + + Roadmap ------- @@ -82,7 +91,6 @@ Many tasks remain before calling this version 1.0: 0.0.2: -- Fix clipping errors - AWT: - Blinking cursor - ECMA48Backend running on socket @@ -131,3 +139,8 @@ Wishlist features (2.0): - TText - TTerminal - TComboBox + + +Screenshots +----------- + diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 9d845be..e710895 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -120,12 +120,33 @@ public class TApplication { break; } } - synchronized (application) { - application.wait(); + + synchronized (this) { + /* + System.err.printf("%s %s sleep\n", this, primary ? + "primary" : "secondary"); + */ + + this.wait(); + + /* + System.err.printf("%s %s AWAKE\n", this, primary ? + "primary" : "secondary"); + */ + if ((!primary) && (application.secondaryEventReceiver == null) ) { - // Secondary thread, time to exit + // Secondary thread, emergency exit. If we + // got here then something went wrong with + // the handoff between yield() and + // closeWindow(). + + System.err.printf("secondary exiting at wrong time, why?\n"); + synchronized (application.primaryEventHandler) { + application.primaryEventHandler.notify(); + } + application.secondaryEventHandler = null; return; } break; @@ -153,6 +174,7 @@ public class TApplication { } else { secondaryHandleEvent(event); } + application.repaint = true; if ((!primary) && (application.secondaryEventReceiver == null) ) { @@ -160,7 +182,15 @@ public class TApplication { // DO NOT UNLOCK. Primary thread just came back from // primaryHandleEvent() and will unlock in the else - // block below. + // block below. Just wake it up. + synchronized (application.primaryEventHandler) { + application.primaryEventHandler.notify(); + } + // Now eliminate my reference so that + // wakeEventHandler() resumes working on the primary. + application.secondaryEventHandler = null; + + // All done! return; } else { // Unlock. Either I am primary thread, or I am @@ -168,7 +198,14 @@ public class TApplication { oldLock = unlockHandleEvent(); assert (oldLock == true); } + } // for (;;) + + // I have done some work of some kind. Tell the main run() + // loop to wake up now. + synchronized (application) { + application.notify(); } + } // while (true) (main runnable loop) } } @@ -176,17 +213,17 @@ public class TApplication { /** * The primary event handler thread. */ - private WidgetEventHandler primaryEventHandler; + private volatile WidgetEventHandler primaryEventHandler; /** * The secondary event handler thread. */ - private WidgetEventHandler secondaryEventHandler; + private volatile WidgetEventHandler secondaryEventHandler; /** * The widget receiving events from the secondary event handler thread. */ - private TWidget secondaryEventReceiver; + private volatile TWidget secondaryEventReceiver; /** * Spinlock for the primary and secondary event handlers. @@ -194,6 +231,22 @@ public class TApplication { */ private volatile boolean insideHandleEvent = false; + /** + * Wake the sleeping active event handler. + */ + private void wakeEventHandler() { + if (secondaryEventHandler != null) { + synchronized (secondaryEventHandler) { + secondaryEventHandler.notify(); + } + } else { + assert (primaryEventHandler != null); + synchronized (primaryEventHandler) { + primaryEventHandler.notify(); + } + } + } + /** * Set the insideHandleEvent flag to true. lockoutEventHandlers() will * spin indefinitely until unlockHandleEvent() is called. @@ -360,22 +413,16 @@ public class TApplication { /** * When true, exit the application. */ - private boolean quit = false; + private volatile boolean quit = false; /** * When true, repaint the entire screen. */ - private boolean repaint = true; - - /** - * Request full repaint on next screen refresh. - */ - public final void setRepaint() { - repaint = true; - } + private volatile boolean repaint = true; /** - * When true, just flush updates from the screen. + * When true, just flush updates from the screen. This is only used to + * minimize physical writes for the mouse cursor. */ private boolean flush = false; @@ -441,9 +488,9 @@ public class TApplication { if (useAWT) { - backend = new AWTBackend(); + backend = new AWTBackend(this); } else { - backend = new ECMA48Backend(input, output); + backend = new ECMA48Backend(this, input, output); } theme = new ColorTheme(); desktopBottom = getScreen().getHeight() - 1; @@ -489,10 +536,6 @@ public class TApplication { return; } - if (!repaint) { - return; - } - if (debugThreads) { System.err.printf("drawAll() REDRAW\n"); } @@ -587,24 +630,48 @@ public class TApplication { while (!quit) { // Timeout is in milliseconds, so default timeout after 1 second // of inactivity. - int timeout = getSleepTime(1000); - - // See if there are any definitely events waiting to be processed - // or a screen redraw to do. If so, do not wait if there is no - // I/O coming in. - synchronized (drainEventQueue) { - if (drainEventQueue.size() > 0) { - timeout = 0; + long timeout = 0; + + // If I've got no updates to render, wait for something from the + // backend or a timer. + if (!repaint && !flush) { + // Never sleep longer than 100 millis, to get windows with + // background tasks an opportunity to update the display. + timeout = getSleepTime(100); + + // See if there are any definitely events waiting to be + // processed. If so, do not wait -- either there is I/O + // coming in or the primary/secondary threads are still + // working. + synchronized (drainEventQueue) { + if (drainEventQueue.size() > 0) { + timeout = 0; + } + } + synchronized (fillEventQueue) { + if (fillEventQueue.size() > 0) { + timeout = 0; + } } } - synchronized (fillEventQueue) { - if (fillEventQueue.size() > 0) { - timeout = 0; + + if (timeout > 0) { + // As of now, I've got nothing to do: no I/O, nothing from + // the consumer threads, no timers that need to run ASAP. So + // wait until either the backend or the consumer threads have + // something to do. + try { + synchronized (this) { + this.wait(timeout); + } + } catch (InterruptedException e) { + // I'm awake and don't care why, let's see what's going + // on out there. } } // Pull any pending I/O events - backend.getEvents(fillEventQueue, timeout); + backend.getEvents(fillEventQueue); // Dispatch each event to the appropriate handler, one at a time. for (;;) { @@ -618,6 +685,13 @@ public class TApplication { metaHandleEvent(event); } + // Wake a consumer thread if we have any pending events. + synchronized (drainEventQueue) { + if (drainEventQueue.size() > 0) { + wakeEventHandler(); + } + } + // Prevent stepping on the primary or secondary event handler. stopEventHandlers(); @@ -631,14 +705,28 @@ public class TApplication { // Let the event handlers run again. startEventHandlers(); - } - // Shutdown the consumer threads - synchronized (this) { - this.notifyAll(); + } // while (!quit) + + // Shutdown the event consumer threads + if (secondaryEventHandler != null) { + synchronized (secondaryEventHandler) { + secondaryEventHandler.notify(); + } + } + if (primaryEventHandler != null) { + synchronized (primaryEventHandler) { + primaryEventHandler.notify(); + } } + // Shutdown the user I/O thread(s) backend.shutdown(); + + // Close all the windows. This gives them an opportunity to release + // resources. + closeAllWindows(); + } /** @@ -697,13 +785,6 @@ public class TApplication { synchronized (drainEventQueue) { drainEventQueue.add(event); } - - // Wake all threads: primary thread will either be consuming events - // again or waiting in yield(), and secondary thread will either not - // exist or consuming events. - synchronized (this) { - this.notifyAll(); - } } /** @@ -861,15 +942,19 @@ public class TApplication { boolean oldLock = unlockHandleEvent(); assert (oldLock == true); + // System.err.printf("YIELD\n"); + while (secondaryEventReceiver != null) { - synchronized (this) { + synchronized (primaryEventHandler) { try { - this.wait(); + primaryEventHandler.wait(); } catch (InterruptedException e) { // SQUASH } } } + + // System.err.printf("EXIT YIELD\n"); } /** @@ -884,7 +969,7 @@ public class TApplication { Date now = new Date(); List keepTimers = new LinkedList(); for (TTimer timer: timers) { - if (timer.getNextTick().getTime() < now.getTime()) { + if (timer.getNextTick().getTime() <= now.getTime()) { timer.tick(); if (timer.recurring) { keepTimers.add(timer); @@ -904,24 +989,27 @@ public class TApplication { /** * Get the amount of time I can sleep before missing a Timer tick. * - * @param timeout = initial (maximum) timeout + * @param timeout = initial (maximum) timeout in millis * @return number of milliseconds between now and the next timer event */ - protected int getSleepTime(final int timeout) { + protected long getSleepTime(final long timeout) { Date now = new Date(); + long nowTime = now.getTime(); long sleepTime = timeout; for (TTimer timer: timers) { - if (timer.getNextTick().getTime() < now.getTime()) { + long nextTickTime = timer.getNextTick().getTime(); + if (nextTickTime < nowTime) { return 0; } - if ((timer.getNextTick().getTime() > now.getTime()) - && ((timer.getNextTick().getTime() - now.getTime()) < sleepTime) - ) { - sleepTime = timer.getNextTick().getTime() - now.getTime(); + + long timeDifference = nextTickTime - nowTime; + if (timeDifference < sleepTime) { + sleepTime = timeDifference; } } assert (sleepTime >= 0); - return (int)sleepTime; + assert (sleepTime <= timeout); + return sleepTime; } /** @@ -963,10 +1051,10 @@ public class TApplication { // window is closed. secondaryEventReceiver = null; - // Wake all threads: primary thread will be consuming events - // again, and secondary thread will exit. - synchronized (this) { - this.notifyAll(); + // Wake the secondary thread, it will wake the primary as it + // exits. + synchronized (secondaryEventHandler) { + secondaryEventHandler.notify(); } } } diff --git a/src/jexer/TButton.java b/src/jexer/TButton.java index 24ee94c..5e5edd0 100644 --- a/src/jexer/TButton.java +++ b/src/jexer/TButton.java @@ -51,6 +51,15 @@ public final class TButton extends TWidget { */ private MnemonicString mnemonic; + /** + * Get the mnemonic string for this button. + * + * @return mnemonic string + */ + public final MnemonicString getMnemonic() { + return mnemonic; + } + /** * Remember mouse state. */ diff --git a/src/jexer/TTerminalWindow.java b/src/jexer/TTerminalWindow.java index 83cc86e..58e60e5 100644 --- a/src/jexer/TTerminalWindow.java +++ b/src/jexer/TTerminalWindow.java @@ -297,7 +297,7 @@ public class TTerminalWindow extends TWindow { // The shell is still running, do nothing. } } - + } // synchronized (emulator) } @@ -386,6 +386,15 @@ public class TTerminalWindow extends TWindow { // Get out of scrollback vScroller.setValue(0); emulator.keypress(keypress.getKey()); + + // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if + // this is kBEnter then also send kbCtrlJ. + if (System.getProperty("os.name").startsWith("Windows")) { + if (keypress.equals(kbEnter)) { + emulator.keypress(kbCtrlJ); + } + } + readEmulatorState(); return; } diff --git a/src/jexer/TTimer.java b/src/jexer/TTimer.java index 0e711b9..442a55c 100644 --- a/src/jexer/TTimer.java +++ b/src/jexer/TTimer.java @@ -81,15 +81,6 @@ public final class TTimer { } } - /** - * Get the number of milliseconds between now and the next tick time. - * - * @return number of millis - */ - public long getMillis() { - return nextTick.getTime() - (new Date()).getTime(); - } - /** * Package private constructor. * @@ -103,7 +94,8 @@ public final class TTimer { this.duration = duration; this.action = action; - nextTick = new Date((new Date()).getTime() + duration); + Date now = new Date(); + nextTick = new Date(now.getTime() + duration); } } diff --git a/src/jexer/TWidget.java b/src/jexer/TWidget.java index d6864db..6cb0f98 100644 --- a/src/jexer/TWidget.java +++ b/src/jexer/TWidget.java @@ -86,13 +86,6 @@ public abstract class TWidget implements Comparable { this.height = height; } - /** - * Request full repaint on next screen refresh. - */ - protected final void setRepaint() { - window.getApplication().setRepaint(); - } - /** * Get this TWidget's parent TApplication. * @@ -487,14 +480,14 @@ public abstract class TWidget implements Comparable { public final void drawChildren() { // Set my clipping rectangle assert (window != null); - assert (window.getScreen() != null); - Screen screen = window.getScreen(); + assert (getScreen() != null); + Screen screen = getScreen(); screen.setClipRight(width); screen.setClipBottom(height); - int absoluteRightEdge = window.getAbsoluteX() + screen.getWidth(); - int absoluteBottomEdge = window.getAbsoluteY() + screen.getHeight(); + int absoluteRightEdge = window.getAbsoluteX() + window.getWidth(); + int absoluteBottomEdge = window.getAbsoluteY() + window.getHeight(); if (!(this instanceof TWindow) && !(this instanceof TVScroller)) { absoluteRightEdge -= 1; } @@ -508,14 +501,14 @@ public abstract class TWidget implements Comparable { screen.setClipRight(0); } else if (myRightEdge > absoluteRightEdge) { screen.setClipRight(screen.getClipRight() - - myRightEdge - absoluteRightEdge); + - (myRightEdge - absoluteRightEdge)); } if (getAbsoluteY() > absoluteBottomEdge) { // I am offscreen screen.setClipBottom(0); } else if (myBottomEdge > absoluteBottomEdge) { screen.setClipBottom(screen.getClipBottom() - - myBottomEdge - absoluteBottomEdge); + - (myBottomEdge - absoluteBottomEdge)); } // Set my offset @@ -727,9 +720,6 @@ public abstract class TWidget implements Comparable { activeChild.active = false; children.get(tabOrder).active = true; activeChild = children.get(tabOrder); - - // Refresh - window.getApplication().setRepaint(); } /** @@ -789,27 +779,25 @@ public abstract class TWidget implements Comparable { // If I have any buttons on me AND this is an Alt-key that matches // its mnemonic, send it an Enter keystroke for (TWidget widget: children) { - /* - TODO - - if (TButton button = cast(TButton)w) { - if (button.enabled && - !keypress.key.isKey && - keypress.key.alt && - !keypress.key.ctrl && - (toLowercase(button.mnemonic.shortcut) == toLowercase(keypress.key.ch))) { - - w.handleEvent(new TKeypressEvent(kbEnter)); + if (widget instanceof TButton) { + TButton button = (TButton) widget; + if (button.getEnabled() + && !keypress.getKey().getIsKey() + && keypress.getKey().getAlt() + && !keypress.getKey().getCtrl() + && (Character.toLowerCase(button.getMnemonic().getShortcut()) + == Character.toLowerCase(keypress.getKey().getCh())) + ) { + + widget.handleEvent(new TKeypressEvent(kbEnter)); return; } } - */ } // Dispatch the keypress to an active widget for (TWidget widget: children) { if (widget.active) { - window.getApplication().setRepaint(); widget.handleEvent(keypress); return; } diff --git a/src/jexer/TWindow.java b/src/jexer/TWindow.java index 65eaa48..0a74a40 100644 --- a/src/jexer/TWindow.java +++ b/src/jexer/TWindow.java @@ -532,7 +532,6 @@ public class TWindow extends TWidget { @Override public void onMouseDown(final TMouseEvent mouse) { this.mouse = mouse; - application.setRepaint(); inKeyboardResize = false; @@ -605,7 +604,6 @@ public class TWindow extends TWidget { @Override public void onMouseUp(final TMouseEvent mouse) { this.mouse = mouse; - application.setRepaint(); if ((inWindowMove) && (mouse.getMouse1())) { // Stop moving window @@ -653,7 +651,6 @@ public class TWindow extends TWidget { @Override public void onMouseMotion(final TMouseEvent mouse) { this.mouse = mouse; - application.setRepaint(); if (inWindowMove) { // Move window over diff --git a/src/jexer/backend/AWTBackend.java b/src/jexer/backend/AWTBackend.java index fce11ea..af2d181 100644 --- a/src/jexer/backend/AWTBackend.java +++ b/src/jexer/backend/AWTBackend.java @@ -49,13 +49,18 @@ public final class AWTBackend extends Backend { /** * Public constructor. + * + * @param listener the object this backend needs to wake up when new + * input comes in */ - public AWTBackend() { + public AWTBackend(final Object listener) { // Create a screen AWTScreen screen = new AWTScreen(); this.screen = screen; - // Create the listeners - terminal = new AWTTerminal(screen); + + // Create the AWT event listeners + terminal = new AWTTerminal(listener, screen); + // Hang onto the session info this.sessionInfo = terminal.getSessionInfo(); } @@ -72,36 +77,10 @@ public final class AWTBackend extends Backend { * Get keyboard, mouse, and screen resize events. * * @param queue list to append new events to - * @param timeout maximum amount of time (in millis) to wait for an - * event. 0 means to return immediately, i.e. perform a poll. */ @Override - public void getEvents(final List queue, final int timeout) { - if (timeout > 0) { - // Try to sleep, let the terminal's input thread wake me up if - // something came in. - synchronized (terminal) { - try { - terminal.wait(timeout); - if (terminal.hasEvents()) { - // System.err.println("getEvents()"); - terminal.getEvents(queue); - } else { - // If I got here, then I timed out. Call - // terminal.getIdleEvents() to pick up stragglers - // like bare resize. - // System.err.println("getIdleEvents()"); - terminal.getIdleEvents(queue); - } - } catch (InterruptedException e) { - // Spurious interrupt, pretend it was like a timeout. - // System.err.println("[interrupt] getEvents()"); - terminal.getIdleEvents(queue); - } - } - } else { - // Asking for a poll, go get it. - // System.err.println("[polled] getEvents()"); + public void getEvents(final List queue) { + if (terminal.hasEvents()) { terminal.getEvents(queue); } } diff --git a/src/jexer/backend/Backend.java b/src/jexer/backend/Backend.java index 3c1bffb..71c2ef0 100644 --- a/src/jexer/backend/Backend.java +++ b/src/jexer/backend/Backend.java @@ -82,10 +82,8 @@ public abstract class Backend { * screen resize events. * * @param queue list to append new events to - * @param timeout maximum amount of time (in millis) to wait for an - * event. 0 means to return immediately, i.e. perform a poll. */ - public abstract void getEvents(List queue, int timeout); + public abstract void getEvents(List queue); /** * Subclasses must provide an implementation that closes sockets, diff --git a/src/jexer/backend/ECMA48Backend.java b/src/jexer/backend/ECMA48Backend.java index da9c9c3..ed25630 100644 --- a/src/jexer/backend/ECMA48Backend.java +++ b/src/jexer/backend/ECMA48Backend.java @@ -53,6 +53,8 @@ public final class ECMA48Backend extends Backend { /** * Public constructor. * + * @param listener the object this backend needs to wake up when new + * input comes in * @param input an InputStream connected to the remote user, or null for * System.in. If System.in is used, then on non-Windows systems it will * be put in raw mode; shutdown() will (blindly!) put System.in in cooked @@ -63,11 +65,11 @@ public final class ECMA48Backend extends Backend { * @throws UnsupportedEncodingException if an exception is thrown when * creating the InputStreamReader */ - public ECMA48Backend(final InputStream input, + public ECMA48Backend(final Object listener, final InputStream input, final OutputStream output) throws UnsupportedEncodingException { // Create a terminal and explicitly set stdin into raw mode - terminal = new ECMA48Terminal(input, output); + terminal = new ECMA48Terminal(listener, input, output); // Keep the terminal's sessionInfo so that TApplication can see it sessionInfo = terminal.getSessionInfo(); @@ -92,29 +94,10 @@ public final class ECMA48Backend extends Backend { * Get keyboard, mouse, and screen resize events. * * @param queue list to append new events to - * @param timeout maximum amount of time (in millis) to wait for an - * event. 0 means to return immediately, i.e. perform a poll. */ @Override - public void getEvents(final List queue, final int timeout) { - if (timeout > 0) { - // Try to sleep, let the terminal's input thread wake me up if - // something came in. - synchronized (terminal) { - try { - terminal.wait(timeout); - } catch (InterruptedException e) { - // Spurious interrupt, pretend it was like a timeout. - // System.err.println("[interrupt] getEvents()"); - } - if (terminal.hasEvents()) { - // System.err.println("getEvents()"); - terminal.getEvents(queue); - } - } - } else { - // Asking for a poll, go get it. - // System.err.println("[polled] getEvents()"); + public void getEvents(final List queue) { + if (terminal.hasEvents()) { terminal.getEvents(queue); } } diff --git a/src/jexer/demos/Demo1.java b/src/jexer/demos/Demo1.java index f0a8c59..1afa354 100644 --- a/src/jexer/demos/Demo1.java +++ b/src/jexer/demos/Demo1.java @@ -422,7 +422,7 @@ class DemoMainWindow extends TWindow { progressBar = addProgressBar(1, row, 22, 0); row++; timerLabel = addLabel("Timer", 1, row); - timer = getApplication().addTimer(100, true, + timer = getApplication().addTimer(250, true, new TAction() { public void DO() { @@ -432,7 +432,6 @@ class DemoMainWindow extends TWindow { timerI++; } progressBar.setValue(timerI); - DemoMainWindow.this.setRepaint(); } } ); diff --git a/src/jexer/io/AWTTerminal.java b/src/jexer/io/AWTTerminal.java index 6cc252f..27ba80a 100644 --- a/src/jexer/io/AWTTerminal.java +++ b/src/jexer/io/AWTTerminal.java @@ -81,6 +81,11 @@ public final class AWTTerminal implements ComponentListener, KeyListener, return sessionInfo; } + /** + * The listening object that run() wakes up on new input. + */ + private Object listener; + /** * The event queue, filled up by a thread reading on input. */ @@ -120,9 +125,12 @@ public final class AWTTerminal implements ComponentListener, KeyListener, /** * Constructor sets up state for getEvent(). * + * @param listener the object this backend needs to wake up when new + * input comes in * @param screen the top-level AWT frame */ - public AWTTerminal(final AWTScreen screen) { + public AWTTerminal(final Object listener, final AWTScreen screen) { + this.listener = listener; this.screen = screen; mouse1 = false; mouse2 = false; @@ -162,26 +170,6 @@ public final class AWTTerminal implements ComponentListener, KeyListener, } } - /** - * Return any events in the IO queue due to timeout. - * - * @param queue list to append new events to - */ - public void getIdleEvents(final List queue) { - - // Insert any polling action here... - - // Return any events that showed up - synchronized (eventQueue) { - if (eventQueue.size() > 0) { - synchronized (queue) { - queue.addAll(eventQueue); - } - eventQueue.clear(); - } - } - } - /** * Pass AWT keystrokes into the event queue. * @@ -396,9 +384,8 @@ public final class AWTTerminal implements ComponentListener, KeyListener, synchronized (eventQueue) { eventQueue.add(new TKeypressEvent(keypress)); } - // Wake up the backend - synchronized (this) { - this.notifyAll(); + synchronized (listener) { + listener.notifyAll(); } } @@ -433,9 +420,8 @@ public final class AWTTerminal implements ComponentListener, KeyListener, synchronized (eventQueue) { eventQueue.add(new TCommandEvent(cmAbort)); } - // Wake up the backend - synchronized (this) { - this.notifyAll(); + synchronized (listener) { + listener.notifyAll(); } } @@ -523,9 +509,8 @@ public final class AWTTerminal implements ComponentListener, KeyListener, sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); eventQueue.add(windowResize); } - // Wake up the backend - synchronized (this) { - this.notifyAll(); + synchronized (listener) { + listener.notifyAll(); } } @@ -561,9 +546,8 @@ public final class AWTTerminal implements ComponentListener, KeyListener, synchronized (eventQueue) { eventQueue.add(mouseEvent); } - // Wake up the backend - synchronized (this) { - this.notifyAll(); + synchronized (listener) { + listener.notifyAll(); } } @@ -582,9 +566,8 @@ public final class AWTTerminal implements ComponentListener, KeyListener, synchronized (eventQueue) { eventQueue.add(mouseEvent); } - // Wake up the backend - synchronized (this) { - this.notifyAll(); + synchronized (listener) { + listener.notifyAll(); } } @@ -650,9 +633,8 @@ public final class AWTTerminal implements ComponentListener, KeyListener, synchronized (eventQueue) { eventQueue.add(mouseEvent); } - // Wake up the backend - synchronized (this) { - this.notifyAll(); + synchronized (listener) { + listener.notifyAll(); } } @@ -697,9 +679,8 @@ public final class AWTTerminal implements ComponentListener, KeyListener, synchronized (eventQueue) { eventQueue.add(mouseEvent); } - // Wake up the backend - synchronized (this) { - this.notifyAll(); + synchronized (listener) { + listener.notifyAll(); } } @@ -743,9 +724,8 @@ public final class AWTTerminal implements ComponentListener, KeyListener, synchronized (eventQueue) { eventQueue.add(mouseEvent); } - // Wake up the backend - synchronized (this) { - this.notifyAll(); + synchronized (listener) { + listener.notifyAll(); } } diff --git a/src/jexer/io/ECMA48Terminal.java b/src/jexer/io/ECMA48Terminal.java index d466646..9e9ffe3 100644 --- a/src/jexer/io/ECMA48Terminal.java +++ b/src/jexer/io/ECMA48Terminal.java @@ -98,11 +98,6 @@ public final class ECMA48Terminal implements Runnable { */ private ArrayList params; - /** - * params[paramI] is being appended to. - */ - private int paramI; - /** * States in the input parser. */ @@ -187,6 +182,11 @@ public final class ECMA48Terminal implements Runnable { */ private PrintWriter output; + /** + * The listening object that run() wakes up on new input. + */ + private Object listener; + /** * When true, the terminal is sending non-UTF8 bytes when reporting mouse * events. @@ -284,6 +284,8 @@ public final class ECMA48Terminal implements Runnable { /** * Constructor sets up state for getEvent(). * + * @param listener the object this backend needs to wake up when new + * input comes in * @param input an InputStream connected to the remote user, or null for * System.in. If System.in is used, then on non-Windows systems it will * be put in raw mode; shutdown() will (blindly!) put System.in in cooked @@ -294,7 +296,7 @@ public final class ECMA48Terminal implements Runnable { * @throws UnsupportedEncodingException if an exception is thrown when * creating the InputStreamReader */ - public ECMA48Terminal(final InputStream input, + public ECMA48Terminal(final Object listener, final InputStream input, final OutputStream output) throws UnsupportedEncodingException { reset(); @@ -302,6 +304,7 @@ public final class ECMA48Terminal implements Runnable { mouse2 = false; mouse3 = false; stopReaderThread = false; + this.listener = listener; if (input == null) { // inputStream = System.in; @@ -402,7 +405,6 @@ public final class ECMA48Terminal implements Runnable { private void reset() { state = ParseState.GROUND; params = new ArrayList(); - paramI = 0; params.clear(); params.add(""); } @@ -799,13 +801,13 @@ public final class ECMA48Terminal implements Runnable { case CSI_ENTRY: // Numbers - parameter values if ((ch >= '0') && (ch <= '9')) { - params.set(paramI, params.get(paramI) + ch); + params.set(params.size() - 1, + params.get(params.size() - 1) + ch); state = ParseState.CSI_PARAM; return; } // Parameter separator if (ch == ';') { - paramI++; params.add(""); return; } @@ -907,14 +909,14 @@ public final class ECMA48Terminal implements Runnable { case CSI_PARAM: // Numbers - parameter values if ((ch >= '0') && (ch <= '9')) { - params.set(paramI, params.get(paramI) + ch); + params.set(params.size() - 1, + params.get(params.size() - 1) + ch); state = ParseState.CSI_PARAM; return; } // Parameter separator if (ch == ';') { - paramI++; - params.add(paramI, ""); + params.add(""); return; } @@ -1000,7 +1002,7 @@ public final class ECMA48Terminal implements Runnable { return; case MOUSE: - params.set(0, params.get(paramI) + ch); + params.set(0, params.get(params.size() - 1) + ch); if (params.get(0).length() == 3) { // We have enough to generate a mouse event events.add(parseMouse()); @@ -1450,9 +1452,8 @@ public final class ECMA48Terminal implements Runnable { synchronized (eventQueue) { eventQueue.addAll(events); } - // Now wake up the backend - synchronized (this) { - this.notifyAll(); + synchronized (listener) { + listener.notifyAll(); } events.clear(); } @@ -1465,6 +1466,9 @@ public final class ECMA48Terminal implements Runnable { eventQueue.addAll(events); } events.clear(); + synchronized (listener) { + listener.notifyAll(); + } } // Wait 10 millis for more data diff --git a/src/jexer/io/Screen.java b/src/jexer/io/Screen.java index 81dabd2..e3f7219 100644 --- a/src/jexer/io/Screen.java +++ b/src/jexer/io/Screen.java @@ -185,6 +185,16 @@ public abstract class Screen { */ protected boolean dirty; + /** + * Get dirty flag. + * + * @return if true, the logical screen is not in sync with the physical + * screen + */ + public final boolean isDirty() { + return dirty; + } + /** * Set if the user explicitly wants to redraw everything starting with a * ECMATerminal.clearAll(). diff --git a/src/jexer/menu/TMenu.java b/src/jexer/menu/TMenu.java index ad8e4ac..ee1af55 100644 --- a/src/jexer/menu/TMenu.java +++ b/src/jexer/menu/TMenu.java @@ -177,7 +177,6 @@ public final class TMenu extends TWindow { @Override public void onMouseDown(final TMouseEvent mouse) { this.mouse = mouse; - setRepaint(); // Pass to children for (TWidget widget: getChildren()) { @@ -202,7 +201,6 @@ public final class TMenu extends TWindow { @Override public void onMouseUp(final TMouseEvent mouse) { this.mouse = mouse; - setRepaint(); // Pass to children for (TWidget widget: getChildren()) { @@ -227,7 +225,6 @@ public final class TMenu extends TWindow { @Override public void onMouseMotion(final TMouseEvent mouse) { this.mouse = mouse; - setRepaint(); // See if we should activate a different menu item for (TWidget widget: getChildren()) { @@ -306,7 +303,6 @@ public final class TMenu extends TWindow { // Dispatch the keypress to an active widget for (TWidget widget: getChildren()) { if (widget.getActive()) { - setRepaint(); widget.handleEvent(keypress); return; } -- 2.27.0