X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTApplication.java;h=e710895b555c6d645f21cc06870a4e25afff0a7a;hb=92554d64c21c6a477fd23a06ca3a64a542b622a3;hp=8786e693271aa6511c747a6d8eb016b640ef1ed7;hpb=d502a0e90eacad7ec676b0abf4686db553b794b1;p=fanfix.git diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 8786e69..e710895 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -36,6 +36,7 @@ import java.io.UnsupportedEncodingException; import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -50,6 +51,7 @@ import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; import jexer.backend.Backend; +import jexer.backend.AWTBackend; import jexer.backend.ECMA48Backend; import jexer.io.Screen; import jexer.menu.TMenu; @@ -62,6 +64,273 @@ import static jexer.TKeypress.*; */ public class TApplication { + /** + * If true, emit thread stuff to System.err. + */ + private static final boolean debugThreads = false; + + /** + * If true, emit events being processed to System.err. + */ + private static final boolean debugEvents = false; + + /** + * WidgetEventHandler is the main event consumer loop. There are at most + * two such threads in existence: the primary for normal case and a + * secondary that is used for TMessageBox, TInputBox, and similar. + */ + private class WidgetEventHandler implements Runnable { + /** + * The main application. + */ + private TApplication application; + + /** + * Whether or not this WidgetEventHandler is the primary or secondary + * thread. + */ + private boolean primary = true; + + /** + * Public constructor. + * + * @param application the main application + * @param primary if true, this is the primary event handler thread + */ + public WidgetEventHandler(final TApplication application, + final boolean primary) { + + this.application = application; + this.primary = primary; + } + + /** + * The consumer loop. + */ + public void run() { + + // Loop forever + while (!application.quit) { + + // Wait until application notifies me + while (!application.quit) { + try { + synchronized (application.drainEventQueue) { + if (application.drainEventQueue.size() > 0) { + break; + } + } + + 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, 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; + } + } catch (InterruptedException e) { + // SQUASH + } + } + + // Pull all events off the queue + for (;;) { + TInputEvent event = null; + synchronized (application.drainEventQueue) { + if (application.drainEventQueue.size() == 0) { + break; + } + event = application.drainEventQueue.remove(0); + } + // Wait for drawAll() or doIdle() to be done, then handle + // the event. + boolean oldLock = lockHandleEvent(); + assert (oldLock == false); + if (primary) { + primaryHandleEvent(event); + } else { + secondaryHandleEvent(event); + } + application.repaint = true; + if ((!primary) + && (application.secondaryEventReceiver == null) + ) { + // Secondary thread, time to exit. + + // DO NOT UNLOCK. Primary thread just came back from + // primaryHandleEvent() and will unlock in the else + // 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 + // 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(); + } + + } // while (true) (main runnable loop) + } + } + + /** + * The primary event handler thread. + */ + private volatile WidgetEventHandler primaryEventHandler; + + /** + * The secondary event handler thread. + */ + private volatile WidgetEventHandler secondaryEventHandler; + + /** + * The widget receiving events from the secondary event handler thread. + */ + 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. + */ + 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. + * + * @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); + + 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); + + 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; + } + /** * Access to the physical screen, keyboard, and mouse. */ @@ -144,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; @@ -209,16 +472,39 @@ public class TApplication { public TApplication(final InputStream input, final OutputStream output) throws UnsupportedEncodingException { - backend = new ECMA48Backend(input, output); + // AWT is the default backend on Windows unless explicitly overridden + // by jexer.AWT. + boolean useAWT = false; + if (System.getProperty("os.name").startsWith("Windows")) { + useAWT = true; + } + if (System.getProperty("jexer.AWT") != null) { + if (System.getProperty("jexer.AWT", "false").equals("true")) { + useAWT = true; + } else { + useAWT = false; + } + } + + + if (useAWT) { + backend = new AWTBackend(this); + } else { + backend = new ECMA48Backend(this, input, output); + } theme = new ColorTheme(); desktopBottom = getScreen().getHeight() - 1; - fillEventQueue = new LinkedList(); - drainEventQueue = new LinkedList(); + fillEventQueue = new ArrayList(); + drainEventQueue = new ArrayList(); windows = new LinkedList(); menus = new LinkedList(); subMenus = new LinkedList(); timers = new LinkedList(); accelerators = new HashMap(); + + // Setup the main consumer thread + primaryEventHandler = new WidgetEventHandler(this, true); + (new Thread(primaryEventHandler)).start(); } /** @@ -240,14 +526,18 @@ public class TApplication { * Draw everything. */ public final void drawAll() { + if (debugThreads) { + System.err.printf("drawAll() enter\n"); + } + if ((flush) && (!repaint)) { backend.flushScreen(); flush = false; return; } - if (!repaint) { - return; + if (debugThreads) { + System.err.printf("drawAll() REDRAW\n"); } // If true, the cursor is not visible @@ -340,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 (;;) { @@ -371,33 +685,48 @@ 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(); + // Process timers and call doIdle()'s doIdle(); // Update the screen - drawAll(); - } + synchronized (getScreen()) { + drawAll(); + } - /* + // Let the event handlers run again. + startEventHandlers(); - // Shutdown the fibers - eventQueue.length = 0; - if (secondaryEventFiber !is null) { - assert(secondaryEventReceiver !is null); - secondaryEventReceiver = null; - if (secondaryEventFiber.state == Fiber.State.HOLD) { - // Wake up the secondary handler so that it can exit. - secondaryEventFiber.call(); + } // while (!quit) + + // Shutdown the event consumer threads + if (secondaryEventHandler != null) { + synchronized (secondaryEventHandler) { + secondaryEventHandler.notify(); } } - - if (primaryEventFiber.state == Fiber.State.HOLD) { - // Wake up the primary handler so that it can exit. - primaryEventFiber.call(); + 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(); + } /** @@ -408,10 +737,10 @@ public class TApplication { */ private void metaHandleEvent(final TInputEvent event) { - /* - System.err.printf(String.format("metaHandleEvents event: %s\n", - event)); System.err.flush(); - */ + if (debugEvents) { + System.err.printf(String.format("metaHandleEvents event: %s\n", + event)); System.err.flush(); + } if (quit) { // Do no more processing if the application is already trying @@ -452,29 +781,10 @@ public class TApplication { } } - // TODO: change to two separate threads - primaryHandleEvent(event); - - /* - // Put into the main queue - addEvent(event); - - // Have one of the two consumer Fibers peel the events off - // the queue. - if (secondaryEventFiber !is null) { - assert(secondaryEventFiber.state == Fiber.State.HOLD); - - // Wake up the secondary handler for these events - secondaryEventFiber.call(); - } else { - assert(primaryEventFiber.state == Fiber.State.HOLD); - - // Wake up the primary handler for these events - primaryEventFiber.call(); - } - */ - + synchronized (drainEventQueue) { + drainEventQueue.add(event); + } } /** @@ -487,7 +797,9 @@ public class TApplication { */ private void primaryHandleEvent(final TInputEvent event) { - // System.err.printf("Handle event: %s\n", event); + if (debugEvents) { + System.err.printf("Handle event: %s\n", event); + } // Special application-wide events ----------------------------------- @@ -580,7 +892,10 @@ public class TApplication { mouse.setX(mouse.getX() - window.getX()); mouse.setY(mouse.getY() - window.getY()); } - // System.err("TApplication dispatch event: %s\n", event); + if (debugEvents) { + System.err.printf("TApplication dispatch event: %s\n", + event); + } window.handleEvent(event); break; } @@ -595,20 +910,68 @@ public class TApplication { * @see #primaryHandleEvent(TInputEvent event) */ private void secondaryHandleEvent(final TInputEvent event) { - // TODO + secondaryEventReceiver.handleEvent(event); + } + + /** + * Enable a widget to override the primary event thread. + * + * @param widget widget that will receive events + */ + public final void enableSecondaryEventReceiver(final TWidget widget) { + assert (secondaryEventReceiver == null); + assert (secondaryEventHandler == null); + assert (widget instanceof TMessageBox); + secondaryEventReceiver = widget; + secondaryEventHandler = new WidgetEventHandler(this, false); + (new Thread(secondaryEventHandler)).start(); + + // Refresh + repaint = true; + } + + /** + * Yield to the secondary thread. + */ + 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 == true); + + // System.err.printf("YIELD\n"); + + while (secondaryEventReceiver != null) { + synchronized (primaryEventHandler) { + try { + primaryEventHandler.wait(); + } catch (InterruptedException e) { + // SQUASH + } + } + } + + // System.err.printf("EXIT YIELD\n"); } /** * Do stuff when there is no user input. */ private void doIdle() { + if (debugThreads) { + System.err.printf("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()) { + if (timer.getNextTick().getTime() <= now.getTime()) { timer.tick(); - if (timer.recurring == true) { + if (timer.recurring) { keepTimers.add(timer); } } else { @@ -626,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; } /** @@ -677,32 +1043,20 @@ public class TApplication { // Refresh screen repaint = true; - /* - TODO - - // Check if we are closing a TMessageBox or similar - if (secondaryEventReceiver !is null) { - assert(secondaryEventFiber !is null); + if (secondaryEventReceiver != null) { + assert (secondaryEventHandler != null); // Do not send events to the secondaryEventReceiver anymore, the // window is closed. secondaryEventReceiver = null; - // Special case: if this is called while executing on a - // secondaryEventFiber, call it so that widgetEventHandler() can - // terminate. - if (secondaryEventFiber.state == Fiber.State.HOLD) { - secondaryEventFiber.call(); - } - secondaryEventFiber = null; - - // Unfreeze the logic in handleEvent() - if (primaryEventFiber.state == Fiber.State.HOLD) { - primaryEventFiber.call(); + // Wake the secondary thread, it will wake the primary as it + // exits. + synchronized (secondaryEventHandler) { + secondaryEventHandler.notify(); } } - */ } /** @@ -993,12 +1347,10 @@ public class TApplication { * @return if true, this event was consumed */ protected boolean onCommand(final TCommandEvent command) { - /* - TODO // Default: handle cmExit if (command.equals(cmExit)) { if (messageBox("Confirmation", "Exit application?", - TMessageBox.Type.YESNO).result == TMessageBox.Result.YES) { + TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { quit = true; } repaint = true; @@ -1006,7 +1358,7 @@ public class TApplication { } if (command.equals(cmShell)) { - openTerminal(0, 0, TWindow.Flag.RESIZABLE); + openTerminal(0, 0, TWindow.RESIZABLE); repaint = true; return true; } @@ -1026,7 +1378,7 @@ public class TApplication { repaint = true; return true; } - */ + return false; } @@ -1041,29 +1393,20 @@ public class TApplication { // Default: handle MID_EXIT if (menu.getId() == TMenu.MID_EXIT) { - /* - TODO if (messageBox("Confirmation", "Exit application?", - TMessageBox.Type.YESNO).result == TMessageBox.Result.YES) { + TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { quit = true; } // System.err.printf("onMenu MID_EXIT result: quit = %s\n", quit); repaint = true; return true; - */ - quit = true; - repaint = true; - return true; } - /* - TODO - if (menu.id == TMenu.MID_SHELL) { - openTerminal(0, 0, TWindow.Flag.RESIZABLE); + if (menu.getId() == TMenu.MID_SHELL) { + openTerminal(0, 0, TWindow.RESIZABLE); repaint = true; return true; } - */ if (menu.getId() == TMenu.MID_TILE) { tileWindows(); @@ -1171,7 +1514,7 @@ public class TApplication { * @param title menu title * @return the new menu */ - public final TMenu addMenu(String title) { + public final TMenu addMenu(final String title) { int x = 0; int y = 0; TMenu menu = new TMenu(this, x, y, title); @@ -1213,7 +1556,7 @@ public class TApplication { * * @return the new menu */ - final public TMenu addWindowMenu() { + public final TMenu addWindowMenu() { TMenu windowMenu = addMenu("&Window"); windowMenu.addDefaultItem(TMenu.MID_TILE); windowMenu.addDefaultItem(TMenu.MID_CASCADE); @@ -1270,8 +1613,6 @@ public class TApplication { int newWidth = (getScreen().getWidth() / a); int newHeight1 = ((getScreen().getHeight() - 1) / b); int newHeight2 = ((getScreen().getHeight() - 1) / (b + c)); - // System.err.printf("Z %s a %s b %s c %s newWidth %s newHeight1 %s newHeight2 %s", - // z, a, b, c, newWidth, newHeight1, newHeight2); List sorted = new LinkedList(windows); Collections.sort(sorted); @@ -1330,6 +1671,7 @@ public class TApplication { * @param duration number of milliseconds to wait between ticks * @param recurring if true, re-schedule this timer after every tick * @param action function to call when button is pressed + * @return the timer */ public final TTimer addTimer(final long duration, final boolean recurring, final TAction action) { @@ -1352,4 +1694,87 @@ public class TApplication { } } + /** + * Convenience function to spawn a message box. + * + * @param title window title, will be centered along the top border + * @param caption message to display. Use embedded newlines to get a + * multi-line box. + * @return the new message box + */ + public final TMessageBox messageBox(final String title, + final String caption) { + + return new TMessageBox(this, title, caption, TMessageBox.Type.OK); + } + + /** + * Convenience function to spawn a message box. + * + * @param title window title, will be centered along the top border + * @param caption message to display. Use embedded newlines to get a + * multi-line box. + * @param type one of the TMessageBox.Type constants. Default is + * Type.OK. + * @return the new message box + */ + public final TMessageBox messageBox(final String title, + final String caption, final TMessageBox.Type type) { + + return new TMessageBox(this, title, caption, type); + } + + /** + * Convenience function to spawn an input box. + * + * @param title window title, will be centered along the top border + * @param caption message to display. Use embedded newlines to get a + * multi-line box. + * @return the new input box + */ + public final TInputBox inputBox(final String title, final String caption) { + + return new TInputBox(this, title, caption); + } + + /** + * Convenience function to spawn an input box. + * + * @param title window title, will be centered along the top border + * @param caption message to display. Use embedded newlines to get a + * multi-line box. + * @param text initial text to seed the field with + * @return the new input box + */ + public final TInputBox inputBox(final String title, final String caption, + final String text) { + + return new TInputBox(this, title, caption, text); + } + + /** + * Convenience function to open a terminal window. + * + * @param x column relative to parent + * @param y row relative to parent + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y) { + return openTerminal(x, y, TWindow.RESIZABLE); + } + + /** + * Convenience function to open a terminal window. + * + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final int flags) { + + return new TTerminalWindow(this, x, y, flags); + } + }