#18 move to event-driven main loop

This commit is contained in:
Kevin Lamonte 2017-08-16 12:46:28 -04:00
parent f6d9020703
commit be72cb5ccb
16 changed files with 483 additions and 387 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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);
}
this.wait();
if (debugThreads) {
System.err.printf("%s %s AWAKE\n", 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();
}
application.secondaryEventHandler = null;
throw new RuntimeException(
"secondary exited at wrong time");
}
if (timeout == 0) {
// A timer needs to fire, break out.
break;
}
if (debugThreads) {
System.err.printf("%d %s %s sleep %d millis\n",
System.currentTimeMillis(), this,
primary ? "primary" : "secondary", timeout);
}
synchronized (this) {
this.wait(timeout);
}
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();
}
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);
// 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<TMenuItem>();
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;
}
synchronized (this) {
boolean doWait = false;
// 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 (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;
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;
}
if (desktop != null) {
desktop.setDimensions(0, 0, resize.getWidth(),
resize.getHeight() - 1);
}
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<TTimer> keepTimers = new LinkedList<TTimer>();
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<TTimer> keepTimers = new LinkedList<TTimer>();
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;

View file

@ -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.
*/

View file

@ -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.
*/

View file

@ -550,6 +550,13 @@ public abstract class TWidget implements Comparable<TWidget> {
}
}
/**
* Repaint the screen on the next update.
*/
public void doRepaint() {
window.getApplication().doRepaint();
}
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
@ -990,7 +997,8 @@ public abstract class TWidget implements Comparable<TWidget> {
/**
* 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

View file

@ -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<TInputEvent> 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<TInputEvent> 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;
}

View file

@ -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;

View file

@ -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;
}
/**

View file

@ -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();
}
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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) {