X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTApplication.java;h=4c55aad9a70f20f83d63fdfb49e9b6b5df8c0e9f;hb=87a17f3ca4b2602c396afdbb13cccb4c1e7cbd38;hp=35aa32d0d0f5dd4c892ab4a3ae79db949b90effe;hpb=8e688b9211599d240be67e5cf62dfe48520378f2;p=nikiroo-utils.git diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 35aa32d..4c55aad 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -34,8 +34,12 @@ import java.io.InputStream; import java.io.OutputStream; 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; import jexer.bits.CellAttributes; import jexer.bits.ColorTheme; @@ -47,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; @@ -59,6 +64,107 @@ import static jexer.TKeypress.*; */ public class TApplication { + /** + * 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 (application) { + application.wait(); + if ((!primary) + && (application.secondaryEventReceiver == null) + ) { + // Secondary thread, time to exit + 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); + } + if (primary) { + primaryHandleEvent(event); + } else { + secondaryHandleEvent(event); + } + if ((!primary) + && (application.secondaryEventReceiver == null) + ) { + // Secondary thread, time to exit + return; + } + } + } // while (true) (main runnable loop) + } + } + + /** + * The primary event handler thread. + */ + private WidgetEventHandler primaryEventHandler; + + /** + * The secondary event handler thread. + */ + private WidgetEventHandler secondaryEventHandler; + + /** + * The widget receiving events from the secondary event handler thread. + */ + private TWidget secondaryEventReceiver; + /** * Access to the physical screen, keyboard, and mouse. */ @@ -109,6 +215,11 @@ public class TApplication { */ private TMenu activeMenu = null; + /** + * Active keyboard accelerators. + */ + private Map accelerators; + /** * Windows and widgets pull colors from this ColorTheme. */ @@ -128,6 +239,11 @@ public class TApplication { */ private List windows; + /** + * Timers that are being ticked. + */ + private List timers; + /** * When true, exit the application. */ @@ -196,14 +312,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(); + } else { + backend = new ECMA48Backend(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(); } /** @@ -360,27 +501,15 @@ public class TApplication { doIdle(); // Update the screen - drawAll(); - } - - /* - - // 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(); + synchronized (getScreen()) { + drawAll(); } } - if (primaryEventFiber.state == Fiber.State.HOLD) { - // Wake up the primary handler so that it can exit. - primaryEventFiber.call(); + // Shutdown the consumer threads + synchronized (this) { + this.notifyAll(); } - */ backend.shutdown(); } @@ -394,8 +523,8 @@ public class TApplication { private void metaHandleEvent(final TInputEvent event) { /* - System.err.printf(String.format("metaHandleEvents event: %s\n", - event)); System.err.flush(); + System.err.printf(String.format("metaHandleEvents event: %s\n", + event)); System.err.flush(); */ if (quit) { @@ -404,16 +533,6 @@ public class TApplication { return; } - // DEBUG - if (event instanceof TKeypressEvent) { - TKeypressEvent keypress = (TKeypressEvent) event; - if (keypress.equals(kbAltX)) { - quit = true; - return; - } - } - // DEBUG - // Special application-wide events ------------------------------- // Abort everything @@ -447,29 +566,17 @@ 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); + } + // Wake all threads: primary thread will either be consuming events + // again or waiting in yield(), and secondary thread will either not + // exist or consuming events. + synchronized (this) { + this.notifyAll(); + } } /** @@ -530,15 +637,16 @@ public class TApplication { return; } - /* - TODO - if (event instanceof TKeypressEvent) { TKeypressEvent keypress = (TKeypressEvent) event; + // See if this key matches an accelerator, and if so dispatch the // menu event. TKeypress keypressLowercase = keypress.getKey().toLowerCase(); - TMenuItem item = accelerators.get(keypressLowercase); + TMenuItem item = null; + synchronized (accelerators) { + item = accelerators.get(keypressLowercase); + } if (item != null) { // Let the menu item dispatch item.dispatch(); @@ -550,7 +658,6 @@ public class TApplication { } } } - */ if (event instanceof TCommandEvent) { if (onCommand((TCommandEvent) event)) { @@ -590,35 +697,65 @@ 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); + while (secondaryEventReceiver != null) { + synchronized (this) { + try { + this.wait(); + } catch (InterruptedException e) { + // SQUASH + } + } + } } /** * Do stuff when there is no user input. */ private void doIdle() { - /* - TODO // Now run any timers that have timed out - auto now = Clock.currTime; - TTimer [] keepTimers; - foreach (t; timers) { - if (t.nextTick < now) { - t.tick(); - if (t.recurring == true) { - keepTimers ~= t; + Date now = new Date(); + List keepTimers = new LinkedList(); + for (TTimer timer: timers) { + if (timer.getNextTick().getTime() < now.getTime()) { + timer.tick(); + if (timer.recurring) { + keepTimers.add(timer); } } else { - keepTimers ~= t; + keepTimers.add(timer); } } timers = keepTimers; // Call onIdle's - foreach (w; windows) { - w.onIdle(); + for (TWindow window: windows) { + window.onIdle(); } - */ } /** @@ -628,24 +765,20 @@ public class TApplication { * @return number of milliseconds between now and the next timer event */ protected int getSleepTime(final int timeout) { - /* - auto now = Clock.currTime; - auto sleepTime = dur!("msecs")(timeout); - foreach (t; timers) { - if (t.nextTick < now) { + Date now = new Date(); + long sleepTime = timeout; + for (TTimer timer: timers) { + if (timer.getNextTick().getTime() < now.getTime()) { return 0; } - if ((t.nextTick > now) && - ((t.nextTick - now) < sleepTime) + if ((timer.getNextTick().getTime() > now.getTime()) + && ((timer.getNextTick().getTime() - now.getTime()) < sleepTime) ) { - sleepTime = t.nextTick - now; + sleepTime = timer.getNextTick().getTime() - now.getTime(); } } - assert(sleepTime.total!("msecs")() >= 0); - return cast(uint)sleepTime.total!("msecs")(); - */ - // TODO: fix timers. Until then, come back after 250 millis. - return 250; + assert (sleepTime >= 0); + return (int)sleepTime; } /** @@ -679,32 +812,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 all threads: primary thread will be consuming events + // again, and secondary thread will exit. + synchronized (this) { + this.notifyAll(); } } - */ } /** @@ -995,23 +1116,24 @@ 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; return true; } + /* + TODO if (command.equals(cmShell)) { openTerminal(0, 0, TWindow.Flag.RESIZABLE); repaint = true; return true; } + */ if (command.equals(cmTile)) { tileWindows(); @@ -1028,7 +1150,7 @@ public class TApplication { repaint = true; return true; } - */ + return false; } @@ -1043,19 +1165,13 @@ 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; } /* @@ -1126,11 +1242,13 @@ public class TApplication { */ public final void addAccelerator(final TMenuItem item, final TKeypress keypress) { - /* - TODO - assert((keypress in accelerators) is null); - accelerators[keypress] = item; - */ + + // System.err.printf("addAccelerator: key %s item %s\n", keypress, item); + + synchronized (accelerators) { + assert (accelerators.get(keypress) == null); + accelerators.put(keypress, item); + } } /** @@ -1171,7 +1289,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 +1331,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); @@ -1310,9 +1428,9 @@ public class TApplication { List sorted = new LinkedList(windows); Collections.sort(sorted); Collections.reverse(sorted); - for (TWindow w: sorted) { - w.setX(x); - w.setY(y); + for (TWindow window: sorted) { + window.setX(x); + window.setY(y); x++; y++; if (x > getScreen().getWidth()) { @@ -1324,4 +1442,91 @@ public class TApplication { } } + /** + * Convenience function to add a timer. + * + * @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) { + + TTimer timer = new TTimer(duration, recurring, action); + synchronized (timers) { + timers.add(timer); + } + return timer; + } + + /** + * Convenience function to remove a timer. + * + * @param timer timer to remove + */ + public final void removeTimer(final TTimer timer) { + synchronized (timers) { + timers.remove(timer); + } + } + + /** + * 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); + } + }