From: Kevin Lamonte Date: Wed, 16 Aug 2017 16:46:28 +0000 (-0400) Subject: #18 move to event-driven main loop X-Git-Url: https://git.nikiroo.be/?a=commitdiff_plain;h=be72cb5ccbd42fe304c0acafc380c5636f0d03a2;p=nikiroo-utils.git #18 move to event-driven main loop --- diff --git a/README.md b/README.md index ff50ca7..7e4a0e5 100644 --- a/README.md +++ b/README.md @@ -205,11 +205,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. - - The JVM needs some warmup time to exhibit the true performance - behavior. Drag a window around for a bit to see this: the initial - performance is slow, then the JIT compiler kicks in and Jexer can - be visually competitive with C/C++ curses applications. - - See jexer.tterminal.ECMA48 for more specifics of terminal emulation limitations. diff --git a/docs/worklog.md b/docs/worklog.md index 757a215..f4a3a36 100644 --- a/docs/worklog.md +++ b/docs/worklog.md @@ -1,6 +1,46 @@ Jexer Work Log ============== +August 16, 2017 + +Holy balls this has gotten so much faster! It is FINALLY visibly +identical in speed to the original d-tui: on xterm it is glass +smooth. CPU load is about +/- 10%, idling around 5%. + +I had to dramatically rework the event processing order, but now it +makes much more sense. TApplication.run()'s sole job is to listen for +backend I/O, push it into drainEventQueue, and wake up the consumer +thread. The consumer thread's run() has the job of dealing with the +event, AND THEN calling doIdles and updating the screen. That was the +big breakthrough: why bother having main thread do screen updates? It +just leads to contention everywhere as it tries to tell the consumer +thread to lay off its data structures, when in reality the consumer +thread should have been the real owner of those structures in the +first place! This was mainly an artifact of the d-tui fiber threading +design. + +So now we have nice flow of events: + +* I/O enters the backend, backend wakes up main thread. + +* Main thread grabs events, wakes up consumer thread. + +* Consumer thread does work, updates screen. + +* Anyone can call doRepaint() to get a screen update shortly + thereafter. + +* Same flow for TTerminalWindow: ECMA48 gets remote I/O, calls back + into TTerminalWindow, which then calls doRepaint(). So in this case + we have a completely external thread asking for a screen update, and + it is working. + +Along the way I also eliminated the Screen.dirty flag and cut out +calls to CellAttribute checks. Overall we now have about 80% less CPU +being burned and way less latency. Both HPROF samples and times puts +my code at roughly 5% of the total, all the rest is the +sleeping/locking infrastructure. + August 15, 2017 I cut 0.0.5 just now, and also applied for a Sonatype repository. diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 09f617e..105f1ce 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -52,6 +52,7 @@ import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; import jexer.backend.Backend; import jexer.backend.Screen; +import jexer.backend.MultiBackend; import jexer.backend.SwingBackend; import jexer.backend.ECMA48Backend; import jexer.backend.TWindowBackend; @@ -145,6 +146,7 @@ public class TApplication implements Runnable { * The consumer loop. */ public void run() { + boolean first = true; // Loop forever while (!application.quit) { @@ -158,44 +160,52 @@ public class TApplication implements Runnable { } } - synchronized (this) { - if (debugThreads) { - System.err.printf("%s %s sleep\n", this, - primary ? "primary" : "secondary"); - } + long timeout = 0; + if (first) { + first = false; + } else { + timeout = application.getSleepTime(1000); + } + + if (timeout == 0) { + // A timer needs to fire, break out. + break; + } - this.wait(); + if (debugThreads) { + System.err.printf("%d %s %s sleep %d millis\n", + System.currentTimeMillis(), this, + primary ? "primary" : "secondary", timeout); + } - if (debugThreads) { - System.err.printf("%s %s AWAKE\n", this, - primary ? "primary" : "secondary"); - } + synchronized (this) { + this.wait(timeout); + } - if ((!primary) - && (application.secondaryEventReceiver == null) - ) { - // Secondary thread, emergency exit. If we - // got here then something went wrong with - // the handoff between yield() and - // closeWindow(). - synchronized (application.primaryEventHandler) { - application.primaryEventHandler.notify(); - } - application.secondaryEventHandler = null; - throw new RuntimeException( - "secondary exited at wrong time"); + if (debugThreads) { + System.err.printf("%d %s %s AWAKE\n", + System.currentTimeMillis(), this, + primary ? "primary" : "secondary"); + } + + if ((!primary) + && (application.secondaryEventReceiver == null) + ) { + // Secondary thread, emergency exit. If we got + // here then something went wrong with the + // handoff between yield() and closeWindow(). + synchronized (application.primaryEventHandler) { + application.primaryEventHandler.notify(); } - break; + application.secondaryEventHandler = null; + throw new RuntimeException("secondary exited " + + "at wrong time"); } + break; } catch (InterruptedException e) { // SQUASH } - } - - // Wait for drawAll() or doIdle() to be done, then handle the - // events. - boolean oldLock = lockHandleEvent(); - assert (oldLock == false); + } // while (!application.quit) // Pull all events off the queue for (;;) { @@ -206,7 +216,11 @@ public class TApplication implements Runnable { } event = application.drainEventQueue.remove(0); } + + // We will have an event to process, so repaint the + // screen at the end. application.repaint = true; + if (primary) { primaryHandleEvent(event); } else { @@ -230,17 +244,12 @@ public class TApplication implements Runnable { // All done! return; } - } // for (;;) - // Unlock. Either I am primary thread, or I am secondary - // thread and still running. - 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(); + // Fire timers, update screen. + if (!quit) { + application.finishEventProcessing(); } } // while (true) (main runnable loop) @@ -262,12 +271,6 @@ public class TApplication implements Runnable { */ private volatile TWidget secondaryEventReceiver; - /** - * Spinlock for the primary and secondary event handlers. - * WidgetEventHandler.run() is responsible for setting this value. - */ - private volatile boolean insideHandleEvent = false; - /** * Wake the sleeping active event handler. */ @@ -284,104 +287,6 @@ public class TApplication implements Runnable { } } - /** - * Set the insideHandleEvent flag to true. lockoutEventHandlers() will - * spin indefinitely until unlockHandleEvent() is called. - * - * @return the old value of insideHandleEvent - */ - private boolean lockHandleEvent() { - if (debugThreads) { - System.err.printf(" >> lockHandleEvent(): oldValue %s", - insideHandleEvent); - } - boolean oldValue = true; - - synchronized (this) { - // Wait for TApplication.run() to finish using the global state - // before allowing further event processing. - while (lockoutHandleEvent == true) { - try { - // Backoff so that the backend can finish its work. - Thread.sleep(5); - } catch (InterruptedException e) { - // SQUASH - } - } - - oldValue = insideHandleEvent; - insideHandleEvent = true; - } - - if (debugThreads) { - System.err.printf(" ***\n"); - } - return oldValue; - } - - /** - * Set the insideHandleEvent flag to false. lockoutEventHandlers() will - * spin indefinitely until unlockHandleEvent() is called. - * - * @return the old value of insideHandleEvent - */ - private boolean unlockHandleEvent() { - if (debugThreads) { - System.err.printf(" << unlockHandleEvent(): oldValue %s\n", - insideHandleEvent); - } - synchronized (this) { - boolean oldValue = insideHandleEvent; - insideHandleEvent = false; - return oldValue; - } - } - - /** - * Spinlock for the primary and secondary event handlers. When true, the - * event handlers will spinlock wait before calling handleEvent(). - */ - private volatile boolean lockoutHandleEvent = false; - - /** - * TApplication.run() needs to be able rely on the global data structures - * being intact when calling doIdle() and drawAll(). Tell the event - * handlers to wait for an unlock before handling their events. - */ - private void stopEventHandlers() { - if (debugThreads) { - System.err.printf(">> stopEventHandlers()"); - } - - lockoutHandleEvent = true; - // Wait for the last event to finish processing before returning - // control to TApplication.run(). - while (insideHandleEvent == true) { - try { - // Backoff so that the event handler can finish its work. - Thread.sleep(1); - } catch (InterruptedException e) { - // SQUASH - } - } - - if (debugThreads) { - System.err.printf(" XXX\n"); - } - } - - /** - * TApplication.run() needs to be able rely on the global data structures - * being intact when calling doIdle() and drawAll(). Tell the event - * handlers that it is now OK to handle their events. - */ - private void startEventHandlers() { - if (debugThreads) { - System.err.printf("<< startEventHandlers()\n"); - } - lockoutHandleEvent = false; - } - // ------------------------------------------------------------------------ // TApplication attributes ------------------------------------------------ // ------------------------------------------------------------------------ @@ -512,6 +417,14 @@ public class TApplication implements Runnable { */ private volatile boolean repaint = true; + /** + * Repaint the screen on the next update. + */ + public void doRepaint() { + repaint = true; + wakeEventHandler(); + } + /** * Y coordinate of the top edge of the desktop. For now this is a * constant. Someday it would be nice to have a multi-line menu or @@ -748,15 +661,56 @@ public class TApplication implements Runnable { menuItems = new ArrayList(); desktop = new TDesktop(this); - // Setup the main consumer thread - primaryEventHandler = new WidgetEventHandler(this, true); - (new Thread(primaryEventHandler)).start(); + // Special case: the Swing backend needs to have a timer to drive its + // blink state. + if ((backend instanceof SwingBackend) + || (backend instanceof MultiBackend) + ) { + // Default to 500 millis, unless a SwingBackend has its own + // value. + long millis = 500; + if (backend instanceof SwingBackend) { + millis = ((SwingBackend) backend).getBlinkMillis(); + } + if (millis > 0) { + addTimer(millis, true, + new TAction() { + public void DO() { + TApplication.this.doRepaint(); + } + } + ); + } + } } // ------------------------------------------------------------------------ // Screen refresh loop ---------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Process background events, and update the screen. + */ + private void finishEventProcessing() { + if (debugThreads) { + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " finishEventProcessing()\n"); + } + + // Process timers and call doIdle()'s + doIdle(); + + // Update the screen + synchronized (getScreen()) { + drawAll(); + } + + if (debugThreads) { + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " finishEventProcessing() END\n"); + } + } + /** * Invert the cell color at a position. This is used to track the mouse. * @@ -765,7 +719,8 @@ public class TApplication implements Runnable { */ private void invertCell(final int x, final int y) { if (debugThreads) { - System.err.printf("invertCell() %d %d\n", x, y); + System.err.printf("%d %s invertCell() %d %d\n", + System.currentTimeMillis(), Thread.currentThread(), x, y); } CellAttributes attr = getScreen().getAttrXY(x, y); attr.setForeColor(attr.getForeColor().invert()); @@ -778,12 +733,14 @@ public class TApplication implements Runnable { */ private void drawAll() { if (debugThreads) { - System.err.printf("drawAll() enter\n"); + System.err.printf("%d %s drawAll() enter\n", + System.currentTimeMillis(), Thread.currentThread()); } if (!repaint) { if (debugThreads) { - System.err.printf("drawAll() !repaint\n"); + System.err.printf("%d %s drawAll() !repaint\n", + System.currentTimeMillis(), Thread.currentThread()); } synchronized (getScreen()) { if ((oldMouseX != mouseX) || (oldMouseY != mouseY)) { @@ -802,7 +759,8 @@ public class TApplication implements Runnable { } if (debugThreads) { - System.err.printf("drawAll() REDRAW\n"); + System.err.printf("%d %s drawAll() REDRAW\n", + System.currentTimeMillis(), Thread.currentThread()); } // If true, the cursor is not visible @@ -897,9 +855,19 @@ public class TApplication implements Runnable { if (sorted.size() > 0) { activeWidget = sorted.get(sorted.size() - 1).getActiveChild(); if (activeWidget.isCursorVisible()) { - getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(), - activeWidget.getCursorAbsoluteY()); - cursor = true; + 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; + } } } @@ -925,68 +893,66 @@ public class TApplication implements Runnable { */ public void exit() { quit = true; + synchronized (this) { + this.notify(); + } } /** * Run this application until it exits. */ public void run() { - boolean first = true; + // Start the main consumer thread + primaryEventHandler = new WidgetEventHandler(this, true); + (new Thread(primaryEventHandler)).start(); while (!quit) { - // Timeout is in milliseconds, so default timeout after 1 second - // of inactivity. - long timeout = 1000; - if (first) { - first = false; - timeout = 0; - } - - // If I've got no updates to render, wait for something from the - // backend or a timer. - if (!repaint - && ((mouseX == oldMouseX) && (mouseY == oldMouseY)) - ) { - // Never sleep longer than 50 millis. We need time for - // windows with background tasks to update the display, and - // still flip buffers reasonably quickly in - // backend.flushPhysical(). - timeout = getSleepTime(50); - } - - 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 { - if (debugThreads) { - System.err.println("sleep " + timeout + " millis"); + synchronized (this) { + boolean doWait = false; + + synchronized (fillEventQueue) { + if (fillEventQueue.size() == 0) { + doWait = true; } - synchronized (this) { - this.wait(timeout); + } + + if (doWait) { + // No I/O to dispatch, so wait until the backend + // provides new I/O. + try { + if (debugThreads) { + System.err.println(System.currentTimeMillis() + + " MAIN sleep"); + } + + this.wait(); + + if (debugThreads) { + System.err.println(System.currentTimeMillis() + + " MAIN AWAKE"); + } + } catch (InterruptedException e) { + // I'm awake and don't care why, let's see what's + // going on out there. } - } catch (InterruptedException e) { - // I'm awake and don't care why, let's see what's going - // on out there. } - repaint = true; - } - // Prevent stepping on the primary or secondary event handler. - stopEventHandlers(); + } // synchronized (this) - // Pull any pending I/O events - backend.getEvents(fillEventQueue); + synchronized (fillEventQueue) { + // Pull any pending I/O events + backend.getEvents(fillEventQueue); - // Dispatch each event to the appropriate handler, one at a time. - for (;;) { - TInputEvent event = null; - if (fillEventQueue.size() == 0) { - break; + // Dispatch each event to the appropriate handler, one at a + // time. + for (;;) { + TInputEvent event = null; + if (fillEventQueue.size() == 0) { + break; + } + event = fillEventQueue.remove(0); + metaHandleEvent(event); } - event = fillEventQueue.remove(0); - metaHandleEvent(event); } // Wake a consumer thread if we have any pending events. @@ -994,17 +960,6 @@ public class TApplication implements Runnable { wakeEventHandler(); } - // Process timers and call doIdle()'s - doIdle(); - - // Update the screen - synchronized (getScreen()) { - drawAll(); - } - - // Let the event handlers run again. - startEventHandlers(); - } // while (!quit) // Shutdown the event consumer threads @@ -1053,32 +1008,34 @@ public class TApplication implements Runnable { if (event instanceof TCommandEvent) { TCommandEvent command = (TCommandEvent) event; if (command.getCmd().equals(cmAbort)) { - quit = true; + exit(); return; } } - // Screen resize - if (event instanceof TResizeEvent) { - TResizeEvent resize = (TResizeEvent) event; - synchronized (getScreen()) { - getScreen().setDimensions(resize.getWidth(), - resize.getHeight()); - desktopBottom = getScreen().getHeight() - 1; - mouseX = 0; - mouseY = 0; - oldMouseX = 0; - oldMouseY = 0; - } - if (desktop != null) { - desktop.setDimensions(0, 0, resize.getWidth(), - resize.getHeight() - 1); + synchronized (drainEventQueue) { + // Screen resize + if (event instanceof TResizeEvent) { + TResizeEvent resize = (TResizeEvent) event; + synchronized (getScreen()) { + getScreen().setDimensions(resize.getWidth(), + resize.getHeight()); + desktopBottom = getScreen().getHeight() - 1; + mouseX = 0; + mouseY = 0; + oldMouseX = 0; + oldMouseY = 0; + } + if (desktop != null) { + desktop.setDimensions(0, 0, resize.getWidth(), + resize.getHeight() - 1); + } + return; } - return; - } - // Put into the main queue - drainEventQueue.add(event); + // Put into the main queue + drainEventQueue.add(event); + } } /** @@ -1262,12 +1219,18 @@ public class TApplication implements Runnable { * @param widget widget that will receive events */ public final void enableSecondaryEventReceiver(final TWidget widget) { + if (debugThreads) { + System.err.println(System.currentTimeMillis() + + " enableSecondaryEventReceiver()"); + } + assert (secondaryEventReceiver == null); assert (secondaryEventHandler == null); assert ((widget instanceof TMessageBox) || (widget instanceof TFileOpenBox)); secondaryEventReceiver = widget; secondaryEventHandler = new WidgetEventHandler(this, false); + (new Thread(secondaryEventHandler)).start(); } @@ -1276,12 +1239,6 @@ public class TApplication implements Runnable { */ public final void yield() { assert (secondaryEventReceiver != null); - // This is where we handoff the event handler lock from the primary - // to secondary thread. We unlock here, and in a future loop the - // secondary thread locks again. When it gives up, we have the - // single lock back. - boolean oldLock = unlockHandleEvent(); - assert (oldLock); while (secondaryEventReceiver != null) { synchronized (primaryEventHandler) { @@ -1299,23 +1256,34 @@ public class TApplication implements Runnable { */ private void doIdle() { if (debugThreads) { - System.err.printf("doIdle()\n"); + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " doIdle()\n"); } - // Now run any timers that have timed out - Date now = new Date(); - List keepTimers = new LinkedList(); - for (TTimer timer: timers) { - if (timer.getNextTick().getTime() <= now.getTime()) { - timer.tick(); - if (timer.recurring) { + synchronized (timers) { + + if (debugThreads) { + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " doIdle() 2\n"); + } + + // Run any timers that have timed out + Date now = new Date(); + List keepTimers = new LinkedList(); + for (TTimer timer: timers) { + if (timer.getNextTick().getTime() <= now.getTime()) { + // Something might change, so repaint the screen. + repaint = true; + timer.tick(); + if (timer.recurring) { + keepTimers.add(timer); + } + } else { keepTimers.add(timer); } - } else { - keepTimers.add(timer); } + timers = keepTimers; } - timers = keepTimers; // Call onIdle's for (TWindow window: windows) { @@ -2322,10 +2290,17 @@ public class TApplication implements Runnable { * @param event new event to add to the queue */ public final void postMenuEvent(final TInputEvent event) { - synchronized (fillEventQueue) { - fillEventQueue.add(event); + synchronized (this) { + synchronized (fillEventQueue) { + fillEventQueue.add(event); + } + if (debugThreads) { + System.err.println(System.currentTimeMillis() + " " + + Thread.currentThread() + " postMenuEvent() wake up main"); + } + closeMenu(); + this.notify(); } - closeMenu(); } /** @@ -2444,7 +2419,7 @@ public class TApplication implements Runnable { if (command.equals(cmExit)) { if (messageBox("Confirmation", "Exit application?", TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { - quit = true; + exit(); } return true; } @@ -2493,7 +2468,7 @@ public class TApplication implements Runnable { if (menu.getId() == TMenu.MID_EXIT) { if (messageBox("Confirmation", "Exit application?", TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { - quit = true; + exit(); } return true; } @@ -2569,17 +2544,21 @@ public class TApplication implements Runnable { Date now = new Date(); long nowTime = now.getTime(); long sleepTime = timeout; - for (TTimer timer: timers) { - long nextTickTime = timer.getNextTick().getTime(); - if (nextTickTime < nowTime) { - return 0; - } - long timeDifference = nextTickTime - nowTime; - if (timeDifference < sleepTime) { - sleepTime = timeDifference; + synchronized (timers) { + for (TTimer timer: timers) { + long nextTickTime = timer.getNextTick().getTime(); + if (nextTickTime < nowTime) { + return 0; + } + + long timeDifference = nextTickTime - nowTime; + if (timeDifference < sleepTime) { + sleepTime = timeDifference; + } } } + assert (sleepTime >= 0); assert (sleepTime <= timeout); return sleepTime; diff --git a/src/jexer/TTerminalWindow.java b/src/jexer/TTerminalWindow.java index 96043e8..4b8bec4 100644 --- a/src/jexer/TTerminalWindow.java +++ b/src/jexer/TTerminalWindow.java @@ -40,13 +40,15 @@ import jexer.event.TKeypressEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; import jexer.tterminal.DisplayLine; +import jexer.tterminal.DisplayListener; import jexer.tterminal.ECMA48; import static jexer.TKeypress.*; /** * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window. */ -public class TTerminalWindow extends TScrollableWindow { +public class TTerminalWindow extends TScrollableWindow + implements DisplayListener { /** * The emulator. @@ -185,6 +187,7 @@ public class TTerminalWindow extends TScrollableWindow { shell = pb.start(); emulator = new ECMA48(deviceType, shell.getInputStream(), shell.getOutputStream()); + emulator.setListener(this); } catch (IOException e) { messageBox("Error", "Error launching shell: " + e.getMessage()); } @@ -323,6 +326,13 @@ public class TTerminalWindow extends TScrollableWindow { } + /** + * Called by emulator when fresh data has come in. + */ + public void displayChanged() { + doRepaint(); + } + /** * Handle window close. */ diff --git a/src/jexer/TTimer.java b/src/jexer/TTimer.java index 3ec63cb..a86c132 100644 --- a/src/jexer/TTimer.java +++ b/src/jexer/TTimer.java @@ -60,6 +60,15 @@ public final class TTimer { return nextTick; } + /** + * Set the recurring flag. + * + * @param recurring if true, re-schedule this timer after every tick + */ + public void setRecurring(final boolean recurring) { + this.recurring = recurring; + } + /** * The action to perfom on a tick. */ diff --git a/src/jexer/TWidget.java b/src/jexer/TWidget.java index 08c0a45..d292ec1 100644 --- a/src/jexer/TWidget.java +++ b/src/jexer/TWidget.java @@ -550,6 +550,13 @@ public abstract class TWidget implements Comparable { } } + /** + * Repaint the screen on the next update. + */ + public void doRepaint() { + window.getApplication().doRepaint(); + } + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -990,7 +997,8 @@ public abstract class TWidget implements Comparable { /** * Method that subclasses can override to do processing when the UI is - * idle. + * idle. Note that repainting is NOT assumed. To get a refresh after + * onIdle, call doRepaint(). */ public void onIdle() { // Default: do nothing, pass to children instead diff --git a/src/jexer/backend/ECMA48Terminal.java b/src/jexer/backend/ECMA48Terminal.java index f23f6f4..ea99a0b 100644 --- a/src/jexer/backend/ECMA48Terminal.java +++ b/src/jexer/backend/ECMA48Terminal.java @@ -40,7 +40,6 @@ import java.io.PrintWriter; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.LinkedList; @@ -712,11 +711,6 @@ public final class ECMA48Terminal extends LogicalScreen * physical screen */ private String flushString() { - if (!dirty) { - assert (!reallyCleared); - return ""; - } - CellAttributes attr = null; StringBuilder sb = new StringBuilder(); @@ -729,7 +723,6 @@ public final class ECMA48Terminal extends LogicalScreen flushLine(y, sb, attr); } - dirty = false; reallyCleared = false; String result = sb.toString(); @@ -1106,10 +1099,10 @@ public final class ECMA48Terminal extends LogicalScreen * @param queue list to append new events to */ private void getIdleEvents(final List queue) { - Date now = new Date(); + long nowTime = System.currentTimeMillis(); // Check for new window size - long windowSizeDelay = now.getTime() - windowSizeTime; + long windowSizeDelay = nowTime - windowSizeTime; if (windowSizeDelay > 1000) { sessionInfo.queryWindowSize(); int newWidth = sessionInfo.getWindowWidth(); @@ -1123,12 +1116,12 @@ public final class ECMA48Terminal extends LogicalScreen newWidth, newHeight); queue.add(event); } - windowSizeTime = now.getTime(); + windowSizeTime = nowTime; } // ESCDELAY type timeout if (state == ParseState.ESCAPE) { - long escDelay = now.getTime() - escapeTime; + long escDelay = nowTime - escapeTime; if (escDelay > 100) { // After 0.1 seconds, assume a true escape character queue.add(controlChar((char)0x1B, false)); @@ -1192,9 +1185,9 @@ public final class ECMA48Terminal extends LogicalScreen private void processChar(final List events, final char ch) { // ESCDELAY type timeout - Date now = new Date(); + long nowTime = System.currentTimeMillis(); if (state == ParseState.ESCAPE) { - long escDelay = now.getTime() - escapeTime; + long escDelay = nowTime - escapeTime; if (escDelay > 250) { // After 0.25 seconds, assume a true escape character events.add(controlChar((char)0x1B, false)); @@ -1214,7 +1207,7 @@ public final class ECMA48Terminal extends LogicalScreen if (ch == 0x1B) { state = ParseState.ESCAPE; - escapeTime = now.getTime(); + escapeTime = nowTime; return; } diff --git a/src/jexer/backend/LogicalScreen.java b/src/jexer/backend/LogicalScreen.java index bfe1c72..4e7971e 100644 --- a/src/jexer/backend/LogicalScreen.java +++ b/src/jexer/backend/LogicalScreen.java @@ -177,11 +177,6 @@ public class LogicalScreen implements Screen { */ protected Cell [][] logical; - /** - * When true, logical != physical. - */ - protected volatile boolean dirty; - /** * Get dirty flag. * @@ -189,7 +184,20 @@ public class LogicalScreen implements Screen { * screen */ public final boolean isDirty() { - return dirty; + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + if (!logical[x][y].equals(physical[x][y])) { + return true; + } + if (logical[x][y].isBlink()) { + // Blinking screens are always dirty. There is + // opportunity for a Netscape blink tag joke here... + return true; + } + } + } + + return false; } /** @@ -284,14 +292,7 @@ public class LogicalScreen implements Screen { } if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) { - dirty = true; - logical[X][Y].setForeColor(attr.getForeColor()); - logical[X][Y].setBackColor(attr.getBackColor()); - logical[X][Y].setBold(attr.isBold()); - logical[X][Y].setBlink(attr.isBlink()); - logical[X][Y].setReverse(attr.isReverse()); - logical[X][Y].setUnderline(attr.isUnderline()); - logical[X][Y].setProtect(attr.isProtect()); + logical[X][Y].setTo(attr); } } @@ -346,20 +347,13 @@ public class LogicalScreen implements Screen { // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch); if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) { - dirty = true; // Do not put control characters on the display assert (ch >= 0x20); assert (ch != 0x7F); + logical[X][Y].setTo(attr); logical[X][Y].setChar(ch); - logical[X][Y].setForeColor(attr.getForeColor()); - logical[X][Y].setBackColor(attr.getBackColor()); - logical[X][Y].setBold(attr.isBold()); - logical[X][Y].setBlink(attr.isBlink()); - logical[X][Y].setReverse(attr.isReverse()); - logical[X][Y].setUnderline(attr.isUnderline()); - logical[X][Y].setProtect(attr.isProtect()); } } @@ -386,7 +380,6 @@ public class LogicalScreen implements Screen { // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch); if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) { - dirty = true; logical[X][Y].setChar(ch); } } @@ -510,7 +503,6 @@ public class LogicalScreen implements Screen { clipBottom = height; reallyCleared = true; - dirty = true; } /** @@ -580,7 +572,6 @@ public class LogicalScreen implements Screen { * clip variables. */ public final synchronized void reset() { - dirty = true; for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { logical[col][row].reset(); @@ -612,7 +603,6 @@ public class LogicalScreen implements Screen { * Clear the physical screen. */ public final void clearPhysical() { - dirty = true; for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { physical[col][row].reset(); @@ -773,6 +763,18 @@ public class LogicalScreen implements Screen { * @param y row coordinate to put the cursor on */ public void putCursor(final boolean visible, final int x, final int y) { + if ((cursorY >= 0) + && (cursorX >= 0) + && (cursorY <= height - 1) + && (cursorX <= width - 1) + ) { + // Make the current cursor position dirty + if (physical[cursorX][cursorY].getChar() == 'Q') { + physical[cursorX][cursorY].setChar('X'); + } else { + physical[cursorX][cursorY].setChar('Q'); + } + } cursorVisible = visible; cursorX = x; diff --git a/src/jexer/backend/MultiScreen.java b/src/jexer/backend/MultiScreen.java index 209105d..735faac 100644 --- a/src/jexer/backend/MultiScreen.java +++ b/src/jexer/backend/MultiScreen.java @@ -182,7 +182,12 @@ public class MultiScreen implements Screen { * screen */ public boolean isDirty() { - return screens.get(0).isDirty(); + for (Screen screen: screens) { + if (screen.isDirty()) { + return true; + } + } + return false; } /** diff --git a/src/jexer/backend/SwingBackend.java b/src/jexer/backend/SwingBackend.java index 281670d..a98627e 100644 --- a/src/jexer/backend/SwingBackend.java +++ b/src/jexer/backend/SwingBackend.java @@ -136,4 +136,15 @@ public final class SwingBackend extends GenericBackend { ((SwingTerminal) terminal).setFont(font); } + /** + * Get the number of millis to wait before switching the blink from + * visible to invisible. + * + * @return the number of milli to wait before switching the blink from + * visible to invisible + */ + public long getBlinkMillis() { + return ((SwingTerminal) terminal).getBlinkMillis(); + } + } diff --git a/src/jexer/backend/SwingTerminal.java b/src/jexer/backend/SwingTerminal.java index 8ac6c2b..b20d448 100644 --- a/src/jexer/backend/SwingTerminal.java +++ b/src/jexer/backend/SwingTerminal.java @@ -36,6 +36,7 @@ import java.awt.Graphics2D; import java.awt.Graphics; import java.awt.Insets; import java.awt.Rectangle; +import java.awt.Toolkit; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.KeyEvent; @@ -50,7 +51,6 @@ import java.awt.event.WindowListener; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.InputStream; -import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -241,11 +241,22 @@ public final class SwingTerminal extends LogicalScreen private CursorStyle cursorStyle = CursorStyle.UNDERLINE; /** - * The number of millis to wait before switching the blink from - * visible to invisible. + * The number of millis to wait before switching the blink from visible + * to invisible. Set to 0 or negative to disable blinking. */ private long blinkMillis = 500; + /** + * Get the number of millis to wait before switching the blink from + * visible to invisible. + * + * @return the number of milli to wait before switching the blink from + * visible to invisible + */ + public long getBlinkMillis() { + return blinkMillis; + } + /** * If true, the cursor should be visible right now based on the blink * time. @@ -663,9 +674,7 @@ public final class SwingTerminal extends LogicalScreen * Reset the blink timer. */ private void resetBlinkTimer() { - // See if it is time to flip the blink time. - long nowTime = (new Date()).getTime(); - lastBlinkTime = nowTime; + lastBlinkTime = System.currentTimeMillis(); cursorBlinkVisible = true; } @@ -678,21 +687,12 @@ public final class SwingTerminal extends LogicalScreen if (gotFontDimensions == false) { // Lazy-load the text width/height - // System.err.println("calling getFontDimensions..."); getFontDimensions(gr); /* System.err.println("textWidth " + textWidth + " textHeight " + textHeight); System.err.println("FONT: " + swing.getFont() + " font " + font); */ - // resizeToScreen(); - } - - // See if it is time to flip the blink time. - long nowTime = (new Date()).getTime(); - if (nowTime > blinkMillis + lastBlinkTime) { - lastBlinkTime = nowTime; - cursorBlinkVisible = !cursorBlinkVisible; } int xCellMin = 0; @@ -762,7 +762,6 @@ public final class SwingTerminal extends LogicalScreen } drawCursor(gr); - dirty = false; reallyCleared = false; } // synchronized (this) } @@ -779,9 +778,39 @@ public final class SwingTerminal extends LogicalScreen */ @Override public void flushPhysical() { + // See if it is time to flip the blink time. + long nowTime = System.currentTimeMillis(); + if (nowTime >= blinkMillis + lastBlinkTime) { + lastBlinkTime = nowTime; + cursorBlinkVisible = !cursorBlinkVisible; + // System.err.println("New lastBlinkTime: " + lastBlinkTime); + } + + if ((swing.getFrame() != null) + && (swing.getBufferStrategy() != null) + ) { + do { + do { + drawToSwing(); + } while (swing.getBufferStrategy().contentsRestored()); + + swing.getBufferStrategy().show(); + Toolkit.getDefaultToolkit().sync(); + } while (swing.getBufferStrategy().contentsLost()); + + } else { + // Non-triple-buffered, call drawToSwing() once + drawToSwing(); + } + } + + /** + * Push the logical screen to the physical device. + */ + private void drawToSwing() { /* - System.err.printf("flushPhysical(): reallyCleared %s dirty %s\n", + System.err.printf("drawToSwing(): reallyCleared %s dirty %s\n", reallyCleared, dirty); */ @@ -795,8 +824,7 @@ public final class SwingTerminal extends LogicalScreen swing.paint(gr); gr.dispose(); swing.getBufferStrategy().show(); - // sync() doesn't seem to help the tearing for me. - // Toolkit.getDefaultToolkit().sync(); + Toolkit.getDefaultToolkit().sync(); return; } else if (((swing.getFrame() != null) && (swing.getBufferStrategy() == null)) @@ -808,19 +836,7 @@ public final class SwingTerminal extends LogicalScreen return; } - // Do nothing if nothing happened. - if (!dirty) { - return; - } - if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) { - // See if it is time to flip the blink time. - long nowTime = (new Date()).getTime(); - if (nowTime > blinkMillis + lastBlinkTime) { - lastBlinkTime = nowTime; - cursorBlinkVisible = !cursorBlinkVisible; - } - Graphics gr = swing.getBufferStrategy().getDrawGraphics(); synchronized (this) { @@ -848,8 +864,7 @@ public final class SwingTerminal extends LogicalScreen gr.dispose(); swing.getBufferStrategy().show(); - // sync() doesn't seem to help the tearing for me. - // Toolkit.getDefaultToolkit().sync(); + Toolkit.getDefaultToolkit().sync(); return; } @@ -916,50 +931,13 @@ public final class SwingTerminal extends LogicalScreen swing.paint(gr); gr.dispose(); swing.getBufferStrategy().show(); - // sync() doesn't seem to help the tearing for me. - // Toolkit.getDefaultToolkit().sync(); + Toolkit.getDefaultToolkit().sync(); } else { // Repaint on the Swing thread. swing.repaint(xMin, yMin, xMax - xMin, yMax - yMin); } } - /** - * Put the cursor at (x,y). - * - * @param visible if true, the cursor should be visible - * @param x column coordinate to put the cursor on - * @param y row coordinate to put the cursor on - */ - @Override - public void putCursor(final boolean visible, final int x, final int y) { - - if ((visible == cursorVisible) && ((x == cursorX) && (y == cursorY))) { - // See if it is time to flip the blink time. - long nowTime = (new Date()).getTime(); - if (nowTime < blinkMillis + lastBlinkTime) { - // Nothing has changed, so don't do anything. - return; - } - } - - if (cursorVisible - && (cursorY >= 0) - && (cursorX >= 0) - && (cursorY <= height - 1) - && (cursorX <= width - 1) - ) { - // Make the current cursor position dirty - if (physical[cursorX][cursorY].getChar() == 'Q') { - physical[cursorX][cursorY].setChar('X'); - } else { - physical[cursorX][cursorY].setChar('Q'); - } - } - - super.putCursor(visible, x, y); - } - /** * Convert pixel column position to text cell column position. * @@ -1265,6 +1243,9 @@ public final class SwingTerminal extends LogicalScreen component.setLayout(new BorderLayout()); component.add(newComponent); + // Allow key events to be received + component.setFocusable(true); + // Get the Swing component SwingTerminal.this.swing = new SwingComponent(component); diff --git a/src/jexer/backend/TTYSessionInfo.java b/src/jexer/backend/TTYSessionInfo.java index e69fc70..22d5314 100644 --- a/src/jexer/backend/TTYSessionInfo.java +++ b/src/jexer/backend/TTYSessionInfo.java @@ -31,7 +31,6 @@ package jexer.backend; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; -import java.util.Date; import java.util.StringTokenizer; /** @@ -64,7 +63,7 @@ public final class TTYSessionInfo implements SessionInfo { /** * Time at which the window size was refreshed. */ - private Date lastQueryWindowTime; + private long lastQueryWindowTime; /** * Username getter. @@ -180,11 +179,11 @@ public final class TTYSessionInfo implements SessionInfo { * Re-query the text window size. */ public void queryWindowSize() { - if (lastQueryWindowTime == null) { - lastQueryWindowTime = new Date(); + if (lastQueryWindowTime == 0) { + lastQueryWindowTime = System.currentTimeMillis(); } else { - Date now = new Date(); - if (now.getTime() - lastQueryWindowTime.getTime() < 3000) { + long nowTime = System.currentTimeMillis(); + if (nowTime - lastQueryWindowTime < 3000) { // Don't re-spawn stty, it's been too soon. return; } diff --git a/src/jexer/demos/Demo6.java b/src/jexer/demos/Demo6.java index c0ec427..9f94913 100644 --- a/src/jexer/demos/Demo6.java +++ b/src/jexer/demos/Demo6.java @@ -84,6 +84,7 @@ public class Demo6 { * one demo application spanning two physical screens. */ multiBackend.addBackend(swingBackend); + multiBackend.setListener(demoApp); /* * Time for the second application. This one will have a single diff --git a/src/jexer/demos/DemoMainWindow.java b/src/jexer/demos/DemoMainWindow.java index 85d0341..209bd13 100644 --- a/src/jexer/demos/DemoMainWindow.java +++ b/src/jexer/demos/DemoMainWindow.java @@ -197,6 +197,8 @@ public class DemoMainWindow extends TWindow { timerLabel.setWidth(timerLabel.getLabel().length()); if (timerI < 100) { timerI++; + } else { + timer.setRecurring(false); } progressBar.setValue(timerI); } diff --git a/src/jexer/tterminal/DisplayListener.java b/src/jexer/tterminal/DisplayListener.java new file mode 100644 index 0000000..fcd8854 --- /dev/null +++ b/src/jexer/tterminal/DisplayListener.java @@ -0,0 +1,42 @@ +/* + * 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.tterminal; + +/** + * DisplayListener is used to callback into external UI when data has come in + * from the remote side. + */ +public interface DisplayListener { + + /** + * Function to call when the display needs to be updated. + */ + public void displayChanged(); + +} diff --git a/src/jexer/tterminal/ECMA48.java b/src/jexer/tterminal/ECMA48.java index 69a9da1..46952da 100644 --- a/src/jexer/tterminal/ECMA48.java +++ b/src/jexer/tterminal/ECMA48.java @@ -302,6 +302,21 @@ public class ECMA48 implements Runnable { } } + /** + * The enclosing listening object. + */ + private DisplayListener listener; + + /** + * Set a listening object. + * + * @param listener the object that will have displayChanged() called + * after bytes are received from the remote terminal + */ + public void setListener(final DisplayListener listener) { + this.listener = listener; + } + /** * When true, the reader thread is expected to exit. */ @@ -6024,6 +6039,10 @@ public class ECMA48 implements Runnable { consume((char)ch); } } + // Permit my enclosing UI to know that I updated. + if (listener != null) { + listener.displayChanged(); + } } // System.err.println("end while loop"); System.err.flush(); } catch (IOException e) {