X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTApplication.java;h=9d27c10f5420052103cee046baae697ca8c2bd6e;hb=12b90437b5f22c2ae6e9b9b14c3b62b60f6143e5;hp=971f6f8fa572ff243645975966243dc9ff25f598;hpb=5dfd1c11947e9cb32fcac4772f1b16879d9ffe67;p=nikiroo-utils.git diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 971f6f8..9d27c10 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2016 Kevin Lamonte + * Copyright (C) 2019 Kevin Lamonte * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -28,23 +28,27 @@ */ package jexer; +import java.io.File; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.io.Reader; import java.io.UnsupportedEncodingException; +import java.text.MessageFormat; +import java.util.ArrayList; 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 java.util.ResourceBundle; +import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.bits.ColorTheme; -import jexer.bits.GraphicsChars; +import jexer.bits.StringUtils; import jexer.event.TCommandEvent; import jexer.event.TInputEvent; import jexer.event.TKeypressEvent; @@ -52,18 +56,33 @@ import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; import jexer.backend.Backend; +import jexer.backend.MultiBackend; +import jexer.backend.Screen; import jexer.backend.SwingBackend; import jexer.backend.ECMA48Backend; -import jexer.io.Screen; +import jexer.backend.TWindowBackend; import jexer.menu.TMenu; import jexer.menu.TMenuItem; +import jexer.menu.TSubMenu; import static jexer.TCommand.*; +import static jexer.TKeypress.*; /** - * TApplication sets up a full Text User Interface application. + * TApplication is the main driver class for a full Text User Interface + * application. It manages windows, provides a menu bar and status bar, and + * processes events received from the user. */ public class TApplication implements Runnable { + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(TApplication.class.getName()); + + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * If true, emit thread stuff to System.err. */ @@ -74,6 +93,12 @@ public class TApplication implements Runnable { */ private static final boolean debugEvents = false; + /** + * If true, do "smart placement" on new windows that are not specified to + * be centered. + */ + private static final boolean smartWindowPlacement = true; + /** * Two backend types are available. */ @@ -94,141 +119,9 @@ public class TApplication implements Runnable { XTERM } - /** - * 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) { - if (debugThreads) { - System.err.printf("%s %s sleep\n", this, - primary ? "primary" : "secondary"); - } - - 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"); - } - break; - } - } catch (InterruptedException e) { - // SQUASH - } - } - - // Wait for drawAll() or doIdle() to be done, then handle the - // events. - boolean oldLock = lockHandleEvent(); - assert (oldLock == false); - - // Pull all events off the queue - for (;;) { - TInputEvent event = null; - synchronized (application.drainEventQueue) { - if (application.drainEventQueue.size() == 0) { - break; - } - event = application.drainEventQueue.remove(0); - } - application.repaint = true; - if (primary) { - primaryHandleEvent(event); - } else { - secondaryHandleEvent(event); - } - 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; - } - } // 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(); - } - - } // while (true) (main runnable loop) - } - } + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ /** * The primary event handler thread. @@ -241,163 +134,65 @@ public class TApplication implements Runnable { 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. + * The screen handler thread. */ - 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(); - } - } - } + private volatile ScreenHandler screenHandler; /** - * Set the insideHandleEvent flag to true. lockoutEventHandlers() will - * spin indefinitely until unlockHandleEvent() is called. - * - * @return the old value of insideHandleEvent + * The widget receiving events from the secondary event handler thread. */ - 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; - } + private volatile TWidget secondaryEventReceiver; /** - * Set the insideHandleEvent flag to false. lockoutEventHandlers() will - * spin indefinitely until unlockHandleEvent() is called. - * - * @return the old value of insideHandleEvent + * Access to the physical screen, keyboard, and mouse. */ - private boolean unlockHandleEvent() { - if (debugThreads) { - System.err.printf(" << unlockHandleEvent(): oldValue %s\n", - insideHandleEvent); - } - synchronized (this) { - boolean oldValue = insideHandleEvent; - insideHandleEvent = false; - return oldValue; - } - } + private Backend backend; /** - * Spinlock for the primary and secondary event handlers. When true, the - * event handlers will spinlock wait before calling handleEvent(). + * Actual mouse coordinate X. */ - private volatile boolean lockoutHandleEvent = false; + private int mouseX; /** - * 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. + * Actual mouse coordinate Y. */ - 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"); - } - } + private int mouseY; /** - * 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. + * Old version of mouse coordinate X. */ - private void startEventHandlers() { - if (debugThreads) { - System.err.printf("<< startEventHandlers()\n"); - } - lockoutHandleEvent = false; - } + private int oldMouseX; /** - * Access to the physical screen, keyboard, and mouse. + * Old version mouse coordinate Y. */ - private Backend backend; + private int oldMouseY; /** - * Get the Screen. - * - * @return the Screen + * Old drawn version of mouse coordinate X. */ - public final Screen getScreen() { - return backend.getScreen(); - } + private int oldDrawnMouseX; /** - * Actual mouse coordinate X. + * Old drawn version mouse coordinate Y. */ - private int mouseX; + private int oldDrawnMouseY; /** - * Actual mouse coordinate Y. + * Old drawn version mouse cell. */ - private int mouseY; + private Cell oldDrawnMouseCell = new Cell(); /** - * Old version of mouse coordinate X. + * The last mouse up click time, used to determine if this is a mouse + * double-click. */ - private int oldMouseX; + private long lastMouseUpTime; /** - * Old version mouse coordinate Y. + * The amount of millis between mouse up events to assume a double-click. */ - private int oldMouseY; + private long doubleClickTime = 250; /** * Event queue that is filled by run(). @@ -421,7 +216,7 @@ public class TApplication implements Runnable { private List subMenus; /** - * The currently acive menu. + * The currently active menu. */ private TMenu activeMenu = null; @@ -441,24 +236,25 @@ public class TApplication implements Runnable { private ColorTheme theme; /** - * Get the color theme. - * - * @return the theme + * The top-level windows (but not menus). */ - public final ColorTheme getTheme() { - return theme; - } + private List windows; /** - * The top-level windows (but not menus). + * The currently acive window. */ - private List windows; + private TWindow activeWindow = null; /** * Timers that are being ticked. */ private List timers; + /** + * When true, the application has been started. + */ + private volatile boolean started = false; + /** * When true, exit the application. */ @@ -474,16 +270,7 @@ public class TApplication implements Runnable { * constant. Someday it would be nice to have a multi-line menu or * toolbars. */ - private static final int desktopTop = 1; - - /** - * Get Y coordinate of the top edge of the desktop. - * - * @return Y coordinate of the top edge of the desktop - */ - public final int getDesktopTop() { - return desktopTop; - } + private int desktopTop = 1; /** * Y coordinate of the bottom edge of the desktop. @@ -491,56 +278,388 @@ public class TApplication implements Runnable { private int desktopBottom; /** - * Get Y coordinate of the bottom edge of the desktop. - * - * @return Y coordinate of the bottom edge of the desktop + * An optional TDesktop background window that is drawn underneath + * everything else. */ - public final int getDesktopBottom() { - return desktopBottom; - } + private TDesktop desktop; /** - * Public constructor. - * - * @param backendType BackendType.XTERM, BackendType.ECMA48 or - * BackendType.SWING - * @throws UnsupportedEncodingException if an exception is thrown when - * creating the InputStreamReader + * If true, focus follows mouse: windows automatically raised if the + * mouse passes over them. */ - public TApplication(final BackendType backendType) - throws UnsupportedEncodingException { + private boolean focusFollowsMouse = false; - switch (backendType) { - case SWING: - backend = new SwingBackend(this); - break; - case XTERM: - // Fall through... - case ECMA48: - backend = new ECMA48Backend(this, null, null); - break; - default: - throw new IllegalArgumentException("Invalid backend type: " - + backendType); - } - TApplicationImpl(); - } + /** + * If true, display a text-based mouse cursor. + */ + private boolean textMouse = true; /** - * Public constructor. The backend type will be BackendType.ECMA48. - * - * @param input an InputStream connected to the remote user, or null for - * System.in. If System.in is used, then on non-Windows systems it will - * be put in raw mode; shutdown() will (blindly!) put System.in in cooked - * mode. input is always converted to a Reader with UTF-8 encoding. - * @param output an OutputStream connected to the remote user, or null - * for System.out. output is always converted to a Writer with UTF-8 - * encoding. - * @throws UnsupportedEncodingException if an exception is thrown when - * creating the InputStreamReader + * If true, hide the mouse after typing a keystroke. */ - public TApplication(final InputStream input, - final OutputStream output) throws UnsupportedEncodingException { + private boolean hideMouseWhenTyping = false; + + /** + * If true, the mouse should not be displayed because a keystroke was + * typed. + */ + private boolean typingHidMouse = false; + + /** + * If true, hide the status bar. + */ + private boolean hideStatusBar = false; + + /** + * If true, hide the menu bar. + */ + private boolean hideMenuBar = false; + + /** + * The list of commands to run before the next I/O check. + */ + private List invokeLaters = new LinkedList(); + + /** + * The last time the screen was resized. + */ + private long screenResizeTime = 0; + + /** + * 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() { + // Wrap everything in a try, so that if we go belly up we can let + // the user have their terminal back. + try { + runImpl(); + } catch (Throwable t) { + this.application.restoreConsole(); + t.printStackTrace(); + this.application.exit(); + } + } + + /** + * The consumer loop. + */ + private void runImpl() { + boolean first = true; + + // Loop forever + while (!application.quit) { + + // Wait until application notifies me + while (!application.quit) { + try { + synchronized (application.drainEventQueue) { + if (application.drainEventQueue.size() > 0) { + break; + } + } + + long timeout = 0; + if (first) { + first = false; + } else { + timeout = application.getSleepTime(1000); + } + + if (timeout == 0) { + // A timer needs to fire, break out. + break; + } + + if (debugThreads) { + System.err.printf("%d %s %s %s sleep %d millis\n", + System.currentTimeMillis(), this, + primary ? "primary" : "secondary", + Thread.currentThread(), timeout); + } + + synchronized (this) { + this.wait(timeout); + } + + if (debugThreads) { + System.err.printf("%d %s %s %s AWAKE\n", + System.currentTimeMillis(), this, + primary ? "primary" : "secondary", + Thread.currentThread()); + } + + 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 + } + } // while (!application.quit) + + // Pull all events off the queue + for (;;) { + TInputEvent event = null; + synchronized (application.drainEventQueue) { + if (application.drainEventQueue.size() == 0) { + break; + } + 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 { + secondaryHandleEvent(event); + } + if ((!primary) + && (application.secondaryEventReceiver == null) + ) { + // Secondary thread, time to exit. + + // Eliminate my reference so that wakeEventHandler() + // resumes working on the primary. + application.secondaryEventHandler = null; + + // We are ready to exit, wake up the primary thread. + // Remember that it is currently sleeping inside its + // primaryHandleEvent(). + synchronized (application.primaryEventHandler) { + application.primaryEventHandler.notify(); + } + + // All done! + return; + } + + } // for (;;) + + // Fire timers, update screen. + if (!quit) { + application.finishEventProcessing(); + } + + } // while (true) (main runnable loop) + } + } + + /** + * ScreenHandler pushes screen updates to the physical device. + */ + private class ScreenHandler implements Runnable { + /** + * The main application. + */ + private TApplication application; + + /** + * The dirty flag. + */ + private boolean dirty = false; + + /** + * Public constructor. + * + * @param application the main application + */ + public ScreenHandler(final TApplication application) { + this.application = application; + } + + /** + * The screen update loop. + */ + public void run() { + // Wrap everything in a try, so that if we go belly up we can let + // the user have their terminal back. + try { + runImpl(); + } catch (Throwable t) { + this.application.restoreConsole(); + t.printStackTrace(); + this.application.exit(); + } + } + + /** + * The update loop. + */ + private void runImpl() { + + // Loop forever + while (!application.quit) { + + // Wait until application notifies me + while (!application.quit) { + try { + synchronized (this) { + if (dirty) { + dirty = false; + break; + } + + // Always check within 50 milliseconds. + this.wait(50); + } + } catch (InterruptedException e) { + // SQUASH + } + } // while (!application.quit) + + // Flush the screen contents + if (debugThreads) { + System.err.printf("%d %s backend.flushScreen()\n", + System.currentTimeMillis(), Thread.currentThread()); + } + synchronized (getScreen()) { + backend.flushScreen(); + } + } // while (true) (main runnable loop) + + // Shutdown the user I/O thread(s) + backend.shutdown(); + } + + /** + * Set the dirty flag. + */ + public void setDirty() { + synchronized (this) { + dirty = true; + } + } + + } + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + * + * @param backendType BackendType.XTERM, BackendType.ECMA48 or + * BackendType.SWING + * @param windowWidth the number of text columns to start with + * @param windowHeight the number of text rows to start with + * @param fontSize the size in points + * @throws UnsupportedEncodingException if an exception is thrown when + * creating the InputStreamReader + */ + public TApplication(final BackendType backendType, final int windowWidth, + final int windowHeight, final int fontSize) + throws UnsupportedEncodingException { + + switch (backendType) { + case SWING: + backend = new SwingBackend(this, windowWidth, windowHeight, + fontSize); + break; + case XTERM: + // Fall through... + case ECMA48: + backend = new ECMA48Backend(this, null, null, windowWidth, + windowHeight, fontSize); + break; + default: + throw new IllegalArgumentException("Invalid backend type: " + + backendType); + } + TApplicationImpl(); + } + + /** + * Public constructor. + * + * @param backendType BackendType.XTERM, BackendType.ECMA48 or + * BackendType.SWING + * @throws UnsupportedEncodingException if an exception is thrown when + * creating the InputStreamReader + */ + public TApplication(final BackendType backendType) + throws UnsupportedEncodingException { + + switch (backendType) { + case SWING: + // The default SwingBackend is 80x25, 20 pt font. If you want to + // change that, you can pass the extra arguments to the + // SwingBackend constructor here. For example, if you wanted + // 90x30, 16 pt font: + // + // backend = new SwingBackend(this, 90, 30, 16); + backend = new SwingBackend(this); + break; + case XTERM: + // Fall through... + case ECMA48: + backend = new ECMA48Backend(this, null, null); + break; + default: + throw new IllegalArgumentException("Invalid backend type: " + + backendType); + } + TApplicationImpl(); + } + + /** + * Public constructor. The backend type will be BackendType.ECMA48. + * + * @param input an InputStream connected to the remote user, or null for + * System.in. If System.in is used, then on non-Windows systems it will + * be put in raw mode; shutdown() will (blindly!) put System.in in cooked + * mode. input is always converted to a Reader with UTF-8 encoding. + * @param output an OutputStream connected to the remote user, or null + * for System.out. output is always converted to a Writer with UTF-8 + * encoding. + * @throws UnsupportedEncodingException if an exception is thrown when + * creating the InputStreamReader + */ + public TApplication(final InputStream input, + final OutputStream output) throws UnsupportedEncodingException { backend = new ECMA48Backend(this, input, output); TApplicationImpl(); @@ -566,714 +685,2112 @@ public class TApplication implements Runnable { } /** - * Public constructor. The backend type will be BackendType.ECMA48. + * Public constructor. The backend type will be BackendType.ECMA48. + * + * @param input the InputStream underlying 'reader'. Its available() + * method is used to determine if reader.read() will block or not. + * @param reader a Reader connected to the remote user. + * @param writer a PrintWriter connected to the remote user. + * @throws IllegalArgumentException if input, reader, or writer are null. + */ + public TApplication(final InputStream input, final Reader reader, + final PrintWriter writer) { + + this(input, reader, writer, false); + } + + /** + * Public constructor. This hook enables use with new non-Jexer + * backends. + * + * @param backend a Backend that is already ready to go. + */ + public TApplication(final Backend backend) { + this.backend = backend; + backend.setListener(this); + TApplicationImpl(); + } + + /** + * Finish construction once the backend is set. + */ + private void TApplicationImpl() { + // Text block mouse option + if (System.getProperty("jexer.textMouse", "true").equals("false")) { + textMouse = false; + } + + // Hide mouse when typing option + if (System.getProperty("jexer.hideMouseWhenTyping", + "false").equals("true")) { + + hideMouseWhenTyping = true; + } + + // Hide status bar option + if (System.getProperty("jexer.hideStatusBar", + "false").equals("true")) { + hideStatusBar = true; + } + + // Hide menu bar option + if (System.getProperty("jexer.hideMenuBar", "false").equals("true")) { + hideMenuBar = true; + } + + theme = new ColorTheme(); + desktopTop = (hideMenuBar ? 0 : 1); + desktopBottom = getScreen().getHeight() - 1 + (hideStatusBar ? 1 : 0); + fillEventQueue = new LinkedList(); + drainEventQueue = new LinkedList(); + windows = new LinkedList(); + menus = new ArrayList(); + subMenus = new ArrayList(); + timers = new LinkedList(); + accelerators = new HashMap(); + menuItems = new LinkedList(); + desktop = new TDesktop(this); + + // 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(); + } + } + ); + } + } + + } + + // ------------------------------------------------------------------------ + // Runnable --------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Run this application until it exits. + */ + public void run() { + // System.err.println("*** TApplication.run() begins ***"); + + // Start the screen updater thread + screenHandler = new ScreenHandler(this); + (new Thread(screenHandler)).start(); + + // Start the main consumer thread + primaryEventHandler = new WidgetEventHandler(this, true); + (new Thread(primaryEventHandler)).start(); + + started = true; + + while (!quit) { + synchronized (this) { + boolean doWait = false; + + if (!backend.hasEvents()) { + synchronized (fillEventQueue) { + if (fillEventQueue.size() == 0) { + doWait = true; + } + } + } + + if (doWait) { + // No I/O to dispatch, so wait until the backend + // provides new I/O. + try { + if (debugThreads) { + System.err.println(System.currentTimeMillis() + + " " + Thread.currentThread() + " MAIN sleep"); + } + + this.wait(); + + if (debugThreads) { + System.err.println(System.currentTimeMillis() + + " " + Thread.currentThread() + " MAIN AWAKE"); + } + } catch (InterruptedException e) { + // I'm awake and don't care why, let's see what's + // going on out there. + } + } + + } // synchronized (this) + + 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; + } + event = fillEventQueue.remove(0); + metaHandleEvent(event); + } + } + + // Wake a consumer thread if we have any pending events. + if (drainEventQueue.size() > 0) { + wakeEventHandler(); + } + + } // while (!quit) + + // Shutdown the event consumer threads + if (secondaryEventHandler != null) { + synchronized (secondaryEventHandler) { + secondaryEventHandler.notify(); + } + } + if (primaryEventHandler != null) { + synchronized (primaryEventHandler) { + primaryEventHandler.notify(); + } + } + + // Close all the windows. This gives them an opportunity to release + // resources. + closeAllWindows(); + + // Close the desktop. + if (desktop != null) { + setDesktop(null); + } + + // Give the overarching application an opportunity to release + // resources. + onExit(); + + // System.err.println("*** TApplication.run() exits ***"); + } + + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Method that TApplication subclasses can override to handle menu or + * posted command events. + * + * @param command command event + * @return if true, this event was consumed + */ + protected boolean onCommand(final TCommandEvent command) { + // Default: handle cmExit + if (command.equals(cmExit)) { + if (messageBox(i18n.getString("exitDialogTitle"), + i18n.getString("exitDialogText"), + TMessageBox.Type.YESNO).isYes()) { + + exit(); + } + return true; + } + + if (command.equals(cmShell)) { + openTerminal(0, 0, TWindow.RESIZABLE); + return true; + } + + if (command.equals(cmTile)) { + tileWindows(); + return true; + } + if (command.equals(cmCascade)) { + cascadeWindows(); + return true; + } + if (command.equals(cmCloseAll)) { + closeAllWindows(); + return true; + } + + if (command.equals(cmMenu) && (hideMenuBar == false)) { + if (!modalWindowActive() && (activeMenu == null)) { + if (menus.size() > 0) { + menus.get(0).setActive(true); + activeMenu = menus.get(0); + return true; + } + } + } + + return false; + } + + /** + * Method that TApplication subclasses can override to handle menu + * events. + * + * @param menu menu event + * @return if true, this event was consumed + */ + protected boolean onMenu(final TMenuEvent menu) { + + // Default: handle MID_EXIT + if (menu.getId() == TMenu.MID_EXIT) { + if (messageBox(i18n.getString("exitDialogTitle"), + i18n.getString("exitDialogText"), + TMessageBox.Type.YESNO).isYes()) { + + exit(); + } + return true; + } + + if (menu.getId() == TMenu.MID_SHELL) { + openTerminal(0, 0, TWindow.RESIZABLE); + return true; + } + + if (menu.getId() == TMenu.MID_TILE) { + tileWindows(); + return true; + } + if (menu.getId() == TMenu.MID_CASCADE) { + cascadeWindows(); + return true; + } + if (menu.getId() == TMenu.MID_CLOSE_ALL) { + closeAllWindows(); + return true; + } + if (menu.getId() == TMenu.MID_ABOUT) { + showAboutDialog(); + return true; + } + if (menu.getId() == TMenu.MID_REPAINT) { + getScreen().clearPhysical(); + doRepaint(); + return true; + } + if (menu.getId() == TMenu.MID_VIEW_IMAGE) { + openImage(); + return true; + } + if (menu.getId() == TMenu.MID_SCREEN_OPTIONS) { + new TFontChooserWindow(this); + return true; + } + return false; + } + + /** + * Method that TApplication subclasses can override to handle keystrokes. + * + * @param keypress keystroke event + * @return if true, this event was consumed + */ + protected boolean onKeypress(final TKeypressEvent keypress) { + // Default: only menu shortcuts + + // Process Alt-F, Alt-E, etc. menu shortcut keys + if (!keypress.getKey().isFnKey() + && keypress.getKey().isAlt() + && !keypress.getKey().isCtrl() + && (activeMenu == null) + && !modalWindowActive() + && (hideMenuBar == false) + ) { + + assert (subMenus.size() == 0); + + for (TMenu menu: menus) { + if (Character.toLowerCase(menu.getMnemonic().getShortcut()) + == Character.toLowerCase(keypress.getKey().getChar()) + ) { + activeMenu = menu; + menu.setActive(true); + return true; + } + } + } + + return false; + } + + /** + * 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(); + } + + // Wake up the screen repainter + wakeScreenHandler(); + + if (debugThreads) { + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " finishEventProcessing() END\n"); + } + } + + /** + * Peek at certain application-level events, add to eventQueue, and wake + * up the consuming Thread. + * + * @param event the input event to consume + */ + private void metaHandleEvent(final TInputEvent event) { + + 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 + // to exit. + return; + } + + // Special application-wide events ------------------------------- + + // Abort everything + if (event instanceof TCommandEvent) { + TCommandEvent command = (TCommandEvent) event; + if (command.equals(cmAbort)) { + exit(); + return; + } + } + + synchronized (drainEventQueue) { + // Screen resize + if (event instanceof TResizeEvent) { + TResizeEvent resize = (TResizeEvent) event; + synchronized (getScreen()) { + if ((System.currentTimeMillis() - screenResizeTime >= 15) + || (resize.getWidth() < getScreen().getWidth()) + || (resize.getHeight() < getScreen().getHeight()) + ) { + getScreen().setDimensions(resize.getWidth(), + resize.getHeight()); + screenResizeTime = System.currentTimeMillis(); + } + desktopBottom = getScreen().getHeight() - 1; + if (hideStatusBar) { + desktopBottom++; + } + mouseX = 0; + mouseY = 0; + oldMouseX = 0; + oldMouseY = 0; + } + if (desktop != null) { + desktop.setDimensions(0, desktopTop, resize.getWidth(), + (desktopBottom - desktopTop)); + desktop.onResize(resize); + } + + // Change menu edges if needed. + recomputeMenuX(); + + // We are dirty, redraw the screen. + doRepaint(); + + /* + System.err.println("New screen: " + resize.getWidth() + + " x " + resize.getHeight()); + */ + return; + } + + // Put into the main queue + drainEventQueue.add(event); + } + } + + /** + * Dispatch one event to the appropriate widget or application-level + * event handler. This is the primary event handler, it has the normal + * application-wide event handling. + * + * @param event the input event to consume + * @see #secondaryHandleEvent(TInputEvent event) + */ + private void primaryHandleEvent(final TInputEvent event) { + + if (debugEvents) { + System.err.printf("%s primaryHandleEvent: %s\n", + Thread.currentThread(), event); + } + TMouseEvent doubleClick = null; + + // Special application-wide events ----------------------------------- + + if (event instanceof TKeypressEvent) { + if (hideMouseWhenTyping) { + typingHidMouse = true; + } + } + + // Peek at the mouse position + if (event instanceof TMouseEvent) { + typingHidMouse = false; + + TMouseEvent mouse = (TMouseEvent) event; + if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) { + oldMouseX = mouseX; + oldMouseY = mouseY; + mouseX = mouse.getX(); + mouseY = mouse.getY(); + } else { + if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) + && (!mouse.isMouseWheelUp()) + && (!mouse.isMouseWheelDown()) + ) { + if ((mouse.getTime().getTime() - lastMouseUpTime) < + doubleClickTime) { + + // This is a double-click. + doubleClick = new TMouseEvent(TMouseEvent.Type. + MOUSE_DOUBLE_CLICK, + mouse.getX(), mouse.getY(), + mouse.getAbsoluteX(), mouse.getAbsoluteY(), + mouse.isMouse1(), mouse.isMouse2(), + mouse.isMouse3(), + mouse.isMouseWheelUp(), mouse.isMouseWheelDown()); + + } else { + // The first click of a potential double-click. + lastMouseUpTime = mouse.getTime().getTime(); + } + } + } + + // See if we need to switch focus to another window or the menu + checkSwitchFocus((TMouseEvent) event); + } + + // Handle menu events + if ((activeMenu != null) && !(event instanceof TCommandEvent)) { + TMenu menu = activeMenu; + + if (event instanceof TMouseEvent) { + TMouseEvent mouse = (TMouseEvent) event; + + while (subMenus.size() > 0) { + TMenu subMenu = subMenus.get(subMenus.size() - 1); + if (subMenu.mouseWouldHit(mouse)) { + break; + } + if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) + && (!mouse.isMouse1()) + && (!mouse.isMouse2()) + && (!mouse.isMouse3()) + && (!mouse.isMouseWheelUp()) + && (!mouse.isMouseWheelDown()) + ) { + break; + } + // We navigated away from a sub-menu, so close it + closeSubMenu(); + } + + // Convert the mouse relative x/y to menu coordinates + assert (mouse.getX() == mouse.getAbsoluteX()); + assert (mouse.getY() == mouse.getAbsoluteY()); + if (subMenus.size() > 0) { + menu = subMenus.get(subMenus.size() - 1); + } + mouse.setX(mouse.getX() - menu.getX()); + mouse.setY(mouse.getY() - menu.getY()); + } + menu.handleEvent(event); + return; + } + + if (event instanceof TKeypressEvent) { + TKeypressEvent keypress = (TKeypressEvent) event; + + // See if this key matches an accelerator, and is not being + // shortcutted by the active window, and if so dispatch the menu + // event. + boolean windowWillShortcut = false; + if (activeWindow != null) { + assert (activeWindow.isShown()); + if (activeWindow.isShortcutKeypress(keypress.getKey())) { + // We do not process this key, it will be passed to the + // window instead. + windowWillShortcut = true; + } + } + + if (!windowWillShortcut && !modalWindowActive()) { + TKeypress keypressLowercase = keypress.getKey().toLowerCase(); + TMenuItem item = null; + synchronized (accelerators) { + item = accelerators.get(keypressLowercase); + } + if (item != null) { + if (item.isEnabled()) { + // Let the menu item dispatch + item.dispatch(); + return; + } + } + + // Handle the keypress + if (onKeypress(keypress)) { + return; + } + } + } + + if (event instanceof TCommandEvent) { + if (onCommand((TCommandEvent) event)) { + return; + } + } + + if (event instanceof TMenuEvent) { + if (onMenu((TMenuEvent) event)) { + return; + } + } + + // Dispatch events to the active window ------------------------------- + boolean dispatchToDesktop = true; + TWindow window = activeWindow; + if (window != null) { + assert (window.isActive()); + assert (window.isShown()); + if (event instanceof TMouseEvent) { + TMouseEvent mouse = (TMouseEvent) event; + // Convert the mouse relative x/y to window coordinates + assert (mouse.getX() == mouse.getAbsoluteX()); + assert (mouse.getY() == mouse.getAbsoluteY()); + mouse.setX(mouse.getX() - window.getX()); + mouse.setY(mouse.getY() - window.getY()); + + if (doubleClick != null) { + doubleClick.setX(doubleClick.getX() - window.getX()); + doubleClick.setY(doubleClick.getY() - window.getY()); + } + + if (window.mouseWouldHit(mouse)) { + dispatchToDesktop = false; + } + } else if (event instanceof TKeypressEvent) { + dispatchToDesktop = false; + } else if (event instanceof TMenuEvent) { + dispatchToDesktop = false; + } + + if (debugEvents) { + System.err.printf("TApplication dispatch event: %s\n", + event); + } + window.handleEvent(event); + if (doubleClick != null) { + window.handleEvent(doubleClick); + } + } + if (dispatchToDesktop) { + // This event is fair game for the desktop to process. + if (desktop != null) { + desktop.handleEvent(event); + if (doubleClick != null) { + desktop.handleEvent(doubleClick); + } + } + } + } + + /** + * Dispatch one event to the appropriate widget or application-level + * event handler. This is the secondary event handler used by certain + * special dialogs (currently TMessageBox and TFileOpenBox). + * + * @param event the input event to consume + * @see #primaryHandleEvent(TInputEvent event) + */ + private void secondaryHandleEvent(final TInputEvent event) { + TMouseEvent doubleClick = null; + + if (debugEvents) { + System.err.printf("%s secondaryHandleEvent: %s\n", + Thread.currentThread(), event); + } + + // Peek at the mouse position + if (event instanceof TMouseEvent) { + typingHidMouse = false; + + TMouseEvent mouse = (TMouseEvent) event; + if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) { + oldMouseX = mouseX; + oldMouseY = mouseY; + mouseX = mouse.getX(); + mouseY = mouse.getY(); + } else { + if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) + && (!mouse.isMouseWheelUp()) + && (!mouse.isMouseWheelDown()) + ) { + if ((mouse.getTime().getTime() - lastMouseUpTime) < + doubleClickTime) { + + // This is a double-click. + doubleClick = new TMouseEvent(TMouseEvent.Type. + MOUSE_DOUBLE_CLICK, + mouse.getX(), mouse.getY(), + mouse.getAbsoluteX(), mouse.getAbsoluteY(), + mouse.isMouse1(), mouse.isMouse2(), + mouse.isMouse3(), + mouse.isMouseWheelUp(), mouse.isMouseWheelDown()); + + } else { + // The first click of a potential double-click. + lastMouseUpTime = mouse.getTime().getTime(); + } + } + } + } + + secondaryEventReceiver.handleEvent(event); + // Note that it is possible for secondaryEventReceiver to be null + // now, because its handleEvent() might have finished out on the + // secondary thread. So put any extra processing inside a null + // check. + if (secondaryEventReceiver != null) { + if (doubleClick != null) { + secondaryEventReceiver.handleEvent(doubleClick); + } + } + } + + /** + * Enable a widget to override the primary event thread. + * + * @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(); + } + + /** + * Yield to the secondary thread. + */ + public final void yield() { + if (debugThreads) { + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " yield()\n"); + } + + assert (secondaryEventReceiver != null); + + while (secondaryEventReceiver != null) { + synchronized (primaryEventHandler) { + try { + primaryEventHandler.wait(); + } catch (InterruptedException e) { + // SQUASH + } + } + } + } + + /** + * Do stuff when there is no user input. + */ + private void doIdle() { + if (debugThreads) { + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " doIdle()\n"); + } + + 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); + } + } + timers.clear(); + timers.addAll(keepTimers); + } + + // Call onIdle's + for (TWindow window: windows) { + window.onIdle(); + } + if (desktop != null) { + desktop.onIdle(); + } + + // Run any invokeLaters + synchronized (invokeLaters) { + for (Runnable invoke: invokeLaters) { + invoke.run(); + } + invokeLaters.clear(); + } + + } + + /** + * Wake the sleeping active event handler. + */ + private void wakeEventHandler() { + if (!started) { + return; + } + + if (secondaryEventHandler != null) { + synchronized (secondaryEventHandler) { + secondaryEventHandler.notify(); + } + } else { + assert (primaryEventHandler != null); + synchronized (primaryEventHandler) { + primaryEventHandler.notify(); + } + } + } + + /** + * Wake the sleeping screen handler. + */ + private void wakeScreenHandler() { + if (!started) { + return; + } + + synchronized (screenHandler) { + screenHandler.notify(); + } + } + + // ------------------------------------------------------------------------ + // TApplication ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Place a command on the run queue, and run it before the next round of + * checking I/O. + * + * @param command the command to run later + */ + public void invokeLater(final Runnable command) { + synchronized (invokeLaters) { + invokeLaters.add(command); + } + doRepaint(); + } + + /** + * Restore the console to sane defaults. This is meant to be used for + * improper exits (e.g. a caught exception in main()), and should not be + * necessary for normal program termination. + */ + public void restoreConsole() { + if (backend != null) { + if (backend instanceof ECMA48Backend) { + backend.shutdown(); + } + } + } + + /** + * Get the Backend. + * + * @return the Backend + */ + public final Backend getBackend() { + return backend; + } + + /** + * Get the Screen. + * + * @return the Screen + */ + public final Screen getScreen() { + if (backend instanceof TWindowBackend) { + // We are being rendered to a TWindow. We can't use its + // getScreen() method because that is how it is rendering to a + // hardware backend somewhere. Instead use its getOtherScreen() + // method. + return ((TWindowBackend) backend).getOtherScreen(); + } else { + return backend.getScreen(); + } + } + + /** + * Get the color theme. + * + * @return the theme + */ + public final ColorTheme getTheme() { + return theme; + } + + /** + * Repaint the screen on the next update. + */ + public void doRepaint() { + repaint = true; + wakeEventHandler(); + } + + /** + * Get Y coordinate of the top edge of the desktop. + * + * @return Y coordinate of the top edge of the desktop + */ + public final int getDesktopTop() { + return desktopTop; + } + + /** + * Get Y coordinate of the bottom edge of the desktop. + * + * @return Y coordinate of the bottom edge of the desktop + */ + public final int getDesktopBottom() { + return desktopBottom; + } + + /** + * Set the TDesktop instance. + * + * @param desktop a TDesktop instance, or null to remove the one that is + * set + */ + public final void setDesktop(final TDesktop desktop) { + if (this.desktop != null) { + this.desktop.onPreClose(); + this.desktop.onUnfocus(); + this.desktop.onClose(); + } + this.desktop = desktop; + } + + /** + * Get the TDesktop instance. + * + * @return the desktop, or null if it is not set + */ + public final TDesktop getDesktop() { + return desktop; + } + + /** + * Get the current active window. + * + * @return the active window, or null if it is not set + */ + public final TWindow getActiveWindow() { + return activeWindow; + } + + /** + * Get a (shallow) copy of the window list. + * + * @return a copy of the list of windows for this application + */ + public final List getAllWindows() { + List result = new ArrayList(); + result.addAll(windows); + return result; + } + + /** + * Get focusFollowsMouse flag. + * + * @return true if focus follows mouse: windows automatically raised if + * the mouse passes over them + */ + public boolean getFocusFollowsMouse() { + return focusFollowsMouse; + } + + /** + * Set focusFollowsMouse flag. + * + * @param focusFollowsMouse if true, focus follows mouse: windows + * automatically raised if the mouse passes over them + */ + public void setFocusFollowsMouse(final boolean focusFollowsMouse) { + this.focusFollowsMouse = focusFollowsMouse; + } + + /** + * Display the about dialog. + */ + protected void showAboutDialog() { + String version = getClass().getPackage().getImplementationVersion(); + if (version == null) { + // This is Java 9+, use a hardcoded string here. + version = "0.3.2"; + } + messageBox(i18n.getString("aboutDialogTitle"), + MessageFormat.format(i18n.getString("aboutDialogText"), version), + TMessageBox.Type.OK); + } + + /** + * Handle the Tool | Open image menu item. + */ + private void openImage() { + try { + List filters = new ArrayList(); + filters.add("^.*\\.[Jj][Pp][Gg]$"); + filters.add("^.*\\.[Jj][Pp][Ee][Gg]$"); + filters.add("^.*\\.[Pp][Nn][Gg]$"); + filters.add("^.*\\.[Gg][Ii][Ff]$"); + filters.add("^.*\\.[Bb][Mm][Pp]$"); + String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN, filters); + if (filename != null) { + new TImageWindow(this, new File(filename)); + } + } catch (IOException e) { + // Show this exception to the user. + new TExceptionDialog(this, e); + } + } + + /** + * Check if application is still running. + * + * @return true if the application is running + */ + public final boolean isRunning() { + if (quit == true) { + return false; + } + return true; + } + + // ------------------------------------------------------------------------ + // Screen refresh loop ---------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Invert the cell color at a position. This is used to track the mouse. + * + * @param x column position + * @param y row position + */ + private void invertCell(final int x, final int y) { + invertCell(x, y, false); + } + + /** + * Invert the cell color at a position. This is used to track the mouse. + * + * @param x column position + * @param y row position + * @param onlyThisCell if true, only invert this cell + */ + private void invertCell(final int x, final int y, + final boolean onlyThisCell) { + + if (debugThreads) { + System.err.printf("%d %s invertCell() %d %d\n", + System.currentTimeMillis(), Thread.currentThread(), x, y); + + if (activeWindow != null) { + System.err.println("activeWindow.hasHiddenMouse() " + + activeWindow.hasHiddenMouse()); + } + } + + // If this cell is on top of a visible window that has requested a + // hidden mouse, bail out. + if ((activeWindow != null) && (activeMenu == null)) { + if ((activeWindow.hasHiddenMouse() == true) + && (x > activeWindow.getX()) + && (x < activeWindow.getX() + activeWindow.getWidth() - 1) + && (y > activeWindow.getY()) + && (y < activeWindow.getY() + activeWindow.getHeight() - 1) + ) { + return; + } + } + + // If this cell is on top of the desktop, and the desktop has + // requested a hidden mouse, bail out. + if ((desktop != null) && (activeWindow == null) && (activeMenu == null)) { + if ((desktop.hasHiddenMouse() == true) + && (x > desktop.getX()) + && (x < desktop.getX() + desktop.getWidth() - 1) + && (y > desktop.getY()) + && (y < desktop.getY() + desktop.getHeight() - 1) + ) { + return; + } + } + + Cell cell = getScreen().getCharXY(x, y); + if (cell.isImage()) { + cell.invertImage(); + } + if (cell.getForeColorRGB() < 0) { + cell.setForeColor(cell.getForeColor().invert()); + } else { + cell.setForeColorRGB(cell.getForeColorRGB() ^ 0x00ffffff); + } + if (cell.getBackColorRGB() < 0) { + cell.setBackColor(cell.getBackColor().invert()); + } else { + cell.setBackColorRGB(cell.getBackColorRGB() ^ 0x00ffffff); + } + getScreen().putCharXY(x, y, cell); + if ((onlyThisCell == true) || (cell.getWidth() == Cell.Width.SINGLE)) { + return; + } + + // This cell is one half of a fullwidth glyph. Invert the other + // half. + if (cell.getWidth() == Cell.Width.LEFT) { + if (x < getScreen().getWidth() - 1) { + Cell rightHalf = getScreen().getCharXY(x + 1, y); + if (rightHalf.getWidth() == Cell.Width.RIGHT) { + invertCell(x + 1, y, true); + return; + } + } + } + if (cell.getWidth() == Cell.Width.RIGHT) { + if (x > 0) { + Cell leftHalf = getScreen().getCharXY(x - 1, y); + if (leftHalf.getWidth() == Cell.Width.LEFT) { + invertCell(x - 1, y, true); + } + } + } + } + + /** + * Draw everything. + */ + private void drawAll() { + boolean menuIsActive = false; + + if (debugThreads) { + System.err.printf("%d %s drawAll() enter\n", + System.currentTimeMillis(), Thread.currentThread()); + } + + // I don't think this does anything useful anymore... + if (!repaint) { + if (debugThreads) { + System.err.printf("%d %s drawAll() !repaint\n", + System.currentTimeMillis(), Thread.currentThread()); + } + if ((oldDrawnMouseX != mouseX) || (oldDrawnMouseY != mouseY)) { + if (debugThreads) { + System.err.printf("%d %s drawAll() !repaint MOUSE\n", + System.currentTimeMillis(), Thread.currentThread()); + } + + // The only thing that has happened is the mouse moved. + + // Redraw the old cell at that position, and save the cell at + // the new mouse position. + if (debugThreads) { + System.err.printf("%d %s restoreImage() %d %d\n", + System.currentTimeMillis(), Thread.currentThread(), + oldDrawnMouseX, oldDrawnMouseY); + } + oldDrawnMouseCell.restoreImage(); + getScreen().putCharXY(oldDrawnMouseX, oldDrawnMouseY, + oldDrawnMouseCell); + oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY); + if (backend instanceof ECMA48Backend) { + // Special case: the entire row containing the mouse has + // to be re-drawn if it has any image data, AND any rows + // in between. + if (oldDrawnMouseY != mouseY) { + for (int i = oldDrawnMouseY; ;) { + getScreen().unsetImageRow(i); + if (i == mouseY) { + break; + } + if (oldDrawnMouseY < mouseY) { + i++; + } else { + i--; + } + } + } else { + getScreen().unsetImageRow(mouseY); + } + } + + if ((textMouse == true) && (typingHidMouse == false)) { + // Draw mouse at the new position. + invertCell(mouseX, mouseY); + } + + oldDrawnMouseX = mouseX; + oldDrawnMouseY = mouseY; + } + if (getScreen().isDirty()) { + screenHandler.setDirty(); + } + return; + } + + if (debugThreads) { + System.err.printf("%d %s drawAll() REDRAW\n", + System.currentTimeMillis(), Thread.currentThread()); + } + + // If true, the cursor is not visible + boolean cursor = false; + + // Start with a clean screen + getScreen().clear(); + + // Draw the desktop + if (desktop != null) { + desktop.drawChildren(); + } + + // Draw each window in reverse Z order + List sorted = new ArrayList(windows); + Collections.sort(sorted); + TWindow topLevel = null; + if (sorted.size() > 0) { + topLevel = sorted.get(0); + } + Collections.reverse(sorted); + for (TWindow window: sorted) { + if (window.isShown()) { + window.drawChildren(); + } + } + + if (hideMenuBar == false) { + + // Draw the blank menubar line - reset the screen clipping first + // so it won't trim it out. + getScreen().resetClipping(); + getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ', + theme.getColor("tmenu")); + // Now draw the menus. + int x = 1; + for (TMenu menu: menus) { + CellAttributes menuColor; + CellAttributes menuMnemonicColor; + if (menu.isActive()) { + menuIsActive = true; + menuColor = theme.getColor("tmenu.highlighted"); + menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted"); + topLevel = menu; + } else { + menuColor = theme.getColor("tmenu"); + menuMnemonicColor = theme.getColor("tmenu.mnemonic"); + } + // Draw the menu title + getScreen().hLineXY(x, 0, + StringUtils.width(menu.getTitle()) + 2, ' ', menuColor); + getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor); + // Draw the highlight character + getScreen().putCharXY(x + 1 + + menu.getMnemonic().getScreenShortcutIdx(), + 0, menu.getMnemonic().getShortcut(), menuMnemonicColor); + + if (menu.isActive()) { + ((TWindow) menu).drawChildren(); + // Reset the screen clipping so we can draw the next + // title. + getScreen().resetClipping(); + } + x += StringUtils.width(menu.getTitle()) + 2; + } + + for (TMenu menu: subMenus) { + // Reset the screen clipping so we can draw the next + // sub-menu. + getScreen().resetClipping(); + ((TWindow) menu).drawChildren(); + } + } + getScreen().resetClipping(); + + if (hideStatusBar == false) { + // Draw the status bar of the top-level window + TStatusBar statusBar = null; + if (topLevel != null) { + statusBar = topLevel.getStatusBar(); + } + if (statusBar != null) { + getScreen().resetClipping(); + statusBar.setWidth(getScreen().getWidth()); + statusBar.setY(getScreen().getHeight() - topLevel.getY()); + statusBar.draw(); + } else { + CellAttributes barColor = new CellAttributes(); + barColor.setTo(getTheme().getColor("tstatusbar.text")); + getScreen().hLineXY(0, desktopBottom, getScreen().getWidth(), + ' ', barColor); + } + } + + // Draw the mouse pointer + if (debugThreads) { + System.err.printf("%d %s restoreImage() %d %d\n", + System.currentTimeMillis(), Thread.currentThread(), + oldDrawnMouseX, oldDrawnMouseY); + } + oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY); + if (backend instanceof ECMA48Backend) { + // Special case: the entire row containing the mouse has to be + // re-drawn if it has any image data, AND any rows in between. + if (oldDrawnMouseY != mouseY) { + for (int i = oldDrawnMouseY; ;) { + getScreen().unsetImageRow(i); + if (i == mouseY) { + break; + } + if (oldDrawnMouseY < mouseY) { + i++; + } else { + i--; + } + } + } else { + getScreen().unsetImageRow(mouseY); + } + } + if ((textMouse == true) && (typingHidMouse == false)) { + invertCell(mouseX, mouseY); + } + oldDrawnMouseX = mouseX; + oldDrawnMouseY = mouseY; + + // Place the cursor if it is visible + if (!menuIsActive) { + + int visibleWindowCount = 0; + for (TWindow window: sorted) { + if (window.isShown()) { + visibleWindowCount++; + } + } + if (visibleWindowCount == 0) { + // No windows are visible, only the desktop. Allow it to + // have the cursor. + if (desktop != null) { + sorted.add(desktop); + } + } + + TWidget activeWidget = null; + if (sorted.size() > 0) { + activeWidget = sorted.get(sorted.size() - 1).getActiveChild(); + int cursorClipTop = desktopTop; + int cursorClipBottom = desktopBottom; + if (activeWidget.isCursorVisible()) { + if ((activeWidget.getCursorAbsoluteY() <= cursorClipBottom) + && (activeWidget.getCursorAbsoluteY() >= cursorClipTop) + ) { + getScreen().putCursor(true, + activeWidget.getCursorAbsoluteX(), + activeWidget.getCursorAbsoluteY()); + cursor = true; + } else { + // Turn off the cursor. Also place it at 0,0. + getScreen().putCursor(false, 0, 0); + cursor = false; + } + } + } + } + + // Kill the cursor + if (!cursor) { + getScreen().hideCursor(); + } + + if (getScreen().isDirty()) { + screenHandler.setDirty(); + } + repaint = false; + } + + /** + * Force this application to exit. + */ + public void exit() { + quit = true; + synchronized (this) { + this.notify(); + } + } + + /** + * Subclasses can use this hook to cleanup resources. Called as the last + * step of TApplication.run(). + */ + public void onExit() { + // Default does nothing. + } + + // ------------------------------------------------------------------------ + // TWindow management ----------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Return the total number of windows. * - * @param input the InputStream underlying 'reader'. Its available() - * method is used to determine if reader.read() will block or not. - * @param reader a Reader connected to the remote user. - * @param writer a PrintWriter connected to the remote user. - * @throws IllegalArgumentException if input, reader, or writer are null. + * @return the total number of windows */ - public TApplication(final InputStream input, final Reader reader, - final PrintWriter writer) { - - this(input, reader, writer, false); + public final int windowCount() { + return windows.size(); } /** - * Public constructor. This hook enables use with new non-Jexer - * backends. + * Return the number of windows that are showing. * - * @param backend a Backend that is already ready to go. + * @return the number of windows that are showing on screen */ - public TApplication(final Backend backend) { - this.backend = backend; - TApplicationImpl(); + public final int shownWindowCount() { + int n = 0; + for (TWindow w: windows) { + if (w.isShown()) { + n++; + } + } + return n; } /** - * Finish construction once the backend is set. + * Return the number of windows that are hidden. + * + * @return the number of windows that are hidden */ - private void TApplicationImpl() { - theme = new ColorTheme(); - desktopBottom = getScreen().getHeight() - 1; - fillEventQueue = new ArrayList(); - drainEventQueue = new ArrayList(); - windows = new LinkedList(); - menus = new LinkedList(); - subMenus = new LinkedList(); - timers = new LinkedList(); - accelerators = new HashMap(); - menuItems = new ArrayList(); - - // Setup the main consumer thread - primaryEventHandler = new WidgetEventHandler(this, true); - (new Thread(primaryEventHandler)).start(); + public final int hiddenWindowCount() { + int n = 0; + for (TWindow w: windows) { + if (w.isHidden()) { + n++; + } + } + return n; } /** - * Invert the cell color at a position. This is used to track the mouse. + * Check if a window instance is in this application's window list. * - * @param x column position - * @param y row position + * @param window window to look for + * @return true if this window is in the list */ - private void invertCell(final int x, final int y) { - if (debugThreads) { - System.err.printf("invertCell() %d %d\n", x, y); + public final boolean hasWindow(final TWindow window) { + if (windows.size() == 0) { + return false; + } + for (TWindow w: windows) { + if (w == window) { + assert (window.getApplication() == this); + return true; + } } - CellAttributes attr = getScreen().getAttrXY(x, y); - attr.setForeColor(attr.getForeColor().invert()); - attr.setBackColor(attr.getBackColor().invert()); - getScreen().putAttrXY(x, y, attr, false); + return false; } /** - * Draw everything. - */ - private void drawAll() { - if (debugThreads) { - System.err.printf("drawAll() enter\n"); + * Activate a window: bring it to the top and have it receive events. + * + * @param window the window to become the new active window + */ + public void activateWindow(final TWindow window) { + if (hasWindow(window) == false) { + /* + * Someone has a handle to a window I don't have. Ignore this + * request. + */ + return; } - if (!repaint) { - if (debugThreads) { - System.err.printf("drawAll() !repaint\n"); - } - synchronized (getScreen()) { - if ((oldMouseX != mouseX) || (oldMouseY != mouseY)) { - // The only thing that has happened is the mouse moved. - // Clear the old position and draw the new position. - invertCell(oldMouseX, oldMouseY); - invertCell(mouseX, mouseY); - oldMouseX = mouseX; - oldMouseY = mouseY; - } - if (getScreen().isDirty()) { - backend.flushScreen(); - } - return; + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); } } - if (debugThreads) { - System.err.printf("drawAll() REDRAW\n"); + assert (windows.size() > 0); + + if (window.isHidden()) { + // Unhiding will also activate. + showWindow(window); + return; } + assert (window.isShown()); - // If true, the cursor is not visible - boolean cursor = false; + if (windows.size() == 1) { + assert (window == windows.get(0)); + if (activeWindow == null) { + activeWindow = window; + window.setZ(0); + activeWindow.setActive(true); + activeWindow.onFocus(); + } - // Start with a clean screen - getScreen().clear(); + assert (window.isActive()); + assert (activeWindow == window); + return; + } - // Draw the background - CellAttributes background = theme.getColor("tapplication.background"); - getScreen().putAll(GraphicsChars.HATCH, background); + if (activeWindow == window) { + assert (window.isActive()); - // Draw each window in reverse Z order - List sorted = new LinkedList(windows); - Collections.sort(sorted); - Collections.reverse(sorted); - for (TWindow window: sorted) { - window.drawChildren(); + // Window is already active, do nothing. + return; } - // Draw the blank menubar line - reset the screen clipping first so - // it won't trim it out. - getScreen().resetClipping(); - getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ', - theme.getColor("tmenu")); - // Now draw the menus. - int x = 1; - for (TMenu menu: menus) { - CellAttributes menuColor; - CellAttributes menuMnemonicColor; - if (menu.isActive()) { - menuColor = theme.getColor("tmenu.highlighted"); - menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted"); - } else { - menuColor = theme.getColor("tmenu"); - menuMnemonicColor = theme.getColor("tmenu.mnemonic"); - } - // Draw the menu title - getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ', - menuColor); - getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor); - // Draw the highlight character - getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(), - 0, menu.getMnemonic().getShortcut(), menuMnemonicColor); - - if (menu.isActive()) { - menu.drawChildren(); - // Reset the screen clipping so we can draw the next title. - getScreen().resetClipping(); + assert (!window.isActive()); + if (activeWindow != null) { + activeWindow.setActive(false); + + // Increment every window Z that is on top of window + for (TWindow w: windows) { + if (w == window) { + continue; + } + if (w.getZ() < window.getZ()) { + w.setZ(w.getZ() + 1); + } } - x += menu.getTitle().length() + 2; + + // Unset activeWindow now before unfocus, so that a window + // lifecycle change inside onUnfocus() doesn't call + // switchWindow() and lead to a stack overflow. + TWindow oldActiveWindow = activeWindow; + activeWindow = null; + oldActiveWindow.onUnfocus(); + } + activeWindow = window; + activeWindow.setZ(0); + activeWindow.setActive(true); + activeWindow.onFocus(); + return; + } + + /** + * Hide a window. + * + * @param window the window to hide + */ + public void hideWindow(final TWindow window) { + if (hasWindow(window) == false) { + /* + * Someone has a handle to a window I don't have. Ignore this + * request. + */ + return; } - for (TMenu menu: subMenus) { - // Reset the screen clipping so we can draw the next sub-menu. - getScreen().resetClipping(); - menu.drawChildren(); + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); + } } - // Draw the mouse pointer - invertCell(mouseX, mouseY); - oldMouseX = mouseX; - oldMouseY = mouseY; + assert (windows.size() > 0); - // Place the cursor if it is visible - TWidget activeWidget = null; - if (sorted.size() > 0) { - activeWidget = sorted.get(sorted.size() - 1).getActiveChild(); - if (activeWidget.isCursorVisible()) { - getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(), - activeWidget.getCursorAbsoluteY()); - cursor = true; + if (!window.hidden) { + if (window == activeWindow) { + if (shownWindowCount() > 1) { + switchWindow(true); + } else { + activeWindow = null; + window.setActive(false); + window.onUnfocus(); + } } + window.hidden = true; + window.onHide(); } + } - // Kill the cursor - if (!cursor) { - getScreen().hideCursor(); + /** + * Show a window. + * + * @param window the window to show + */ + public void showWindow(final TWindow window) { + if (hasWindow(window) == false) { + /* + * Someone has a handle to a window I don't have. Ignore this + * request. + */ + return; } - // Flush the screen contents - if (getScreen().isDirty()) { - backend.flushScreen(); + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); + } } - repaint = false; + assert (windows.size() > 0); + + if (window.hidden) { + window.hidden = false; + window.onShow(); + activateWindow(window); + } } /** - * Run this application until it exits. + * Close window. Note that the window's destructor is NOT called by this + * method, instead the GC is assumed to do the cleanup. + * + * @param window the window to remove */ - public void run() { - while (!quit) { - // Timeout is in milliseconds, so default timeout after 1 second - // of inactivity. - long 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) { - this.wait(timeout); - } - } catch (InterruptedException e) { - // I'm awake and don't care why, let's see what's going - // on out there. + public final void closeWindow(final TWindow window) { + if (hasWindow(window) == false) { + /* + * Someone has a handle to a window I don't have. Ignore this + * request. + */ + return; + } + + // Let window know that it is about to be closed, while it is still + // visible on screen. + window.onPreClose(); + + synchronized (windows) { + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); } - repaint = true; } - // Prevent stepping on the primary or secondary event handler. - stopEventHandlers(); + int z = window.getZ(); + window.setZ(-1); + window.onUnfocus(); + windows.remove(window); + Collections.sort(windows); + activeWindow = null; + int newZ = 0; + boolean foundNextWindow = false; - // Pull any pending I/O events - backend.getEvents(fillEventQueue); + for (TWindow w: windows) { + w.setZ(newZ); + newZ++; - // Dispatch each event to the appropriate handler, one at a time. - for (;;) { - TInputEvent event = null; - if (fillEventQueue.size() == 0) { - break; + // Do not activate a hidden window. + if (w.isHidden()) { + continue; } - event = fillEventQueue.remove(0); - metaHandleEvent(event); - } - // Wake a consumer thread if we have any pending events. - if (drainEventQueue.size() > 0) { - wakeEventHandler(); - } - - // Process timers and call doIdle()'s - doIdle(); + if (foundNextWindow == false) { + foundNextWindow = true; + w.setActive(true); + w.onFocus(); + assert (activeWindow == null); + activeWindow = w; + continue; + } - // Update the screen - synchronized (getScreen()) { - drawAll(); + if (w.isActive()) { + w.setActive(false); + w.onUnfocus(); + } } + } - // Let the event handlers run again. - startEventHandlers(); + // Perform window cleanup + window.onClose(); - } // while (!quit) + // Check if we are closing a TMessageBox or similar + if (secondaryEventReceiver != null) { + assert (secondaryEventHandler != null); - // Shutdown the event consumer threads - if (secondaryEventHandler != null) { + // Do not send events to the secondaryEventReceiver anymore, the + // window is closed. + secondaryEventReceiver = null; + + // Wake the secondary thread, it will wake the primary as it + // exits. synchronized (secondaryEventHandler) { secondaryEventHandler.notify(); } } - if (primaryEventHandler != null) { - synchronized (primaryEventHandler) { - primaryEventHandler.notify(); + + // Permit desktop to be active if it is the only thing left. + if (desktop != null) { + if (windows.size() == 0) { + desktop.setActive(true); } } - - // Shutdown the user I/O thread(s) - backend.shutdown(); - - // Close all the windows. This gives them an opportunity to release - // resources. - closeAllWindows(); - } /** - * Peek at certain application-level events, add to eventQueue, and wake - * up the consuming Thread. + * Switch to the next window. * - * @param event the input event to consume + * @param forward if true, then switch to the next window in the list, + * otherwise switch to the previous window in the list */ - private void metaHandleEvent(final TInputEvent event) { - - 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 - // to exit. + public final void switchWindow(final boolean forward) { + // Only switch if there are multiple visible windows + if (shownWindowCount() < 2) { return; } + assert (activeWindow != null); - // Special application-wide events ------------------------------- - - // Abort everything - if (event instanceof TCommandEvent) { - TCommandEvent command = (TCommandEvent) event; - if (command.getCmd().equals(cmAbort)) { - quit = true; - 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 (windows) { + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); + } } - return; - } - // Peek at the mouse position - if (event instanceof TMouseEvent) { - TMouseEvent mouse = (TMouseEvent) event; - synchronized (getScreen()) { - if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) { - oldMouseX = mouseX; - oldMouseY = mouseY; - mouseX = mouse.getX(); - mouseY = mouse.getY(); + // Swap z/active between active window and the next in the list + int activeWindowI = -1; + for (int i = 0; i < windows.size(); i++) { + if (windows.get(i) == activeWindow) { + assert (activeWindow.isActive()); + activeWindowI = i; + break; + } else { + assert (!windows.get(0).isActive()); } } - } - - // Put into the main queue - drainEventQueue.add(event); - } - - /** - * Dispatch one event to the appropriate widget or application-level - * event handler. This is the primary event handler, it has the normal - * application-wide event handling. - * - * @param event the input event to consume - * @see #secondaryHandleEvent(TInputEvent event) - */ - private void primaryHandleEvent(final TInputEvent event) { - - if (debugEvents) { - System.err.printf("Handle event: %s\n", event); - } - - // Special application-wide events ----------------------------------- - - // Peek at the mouse position - if (event instanceof TMouseEvent) { - // See if we need to switch focus to another window or the menu - checkSwitchFocus((TMouseEvent) event); - } - - // Handle menu events - if ((activeMenu != null) && !(event instanceof TCommandEvent)) { - TMenu menu = activeMenu; - - if (event instanceof TMouseEvent) { - TMouseEvent mouse = (TMouseEvent) event; + assert (activeWindowI >= 0); - while (subMenus.size() > 0) { - TMenu subMenu = subMenus.get(subMenus.size() - 1); - if (subMenu.mouseWouldHit(mouse)) { - break; - } - if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) - && (!mouse.isMouse1()) - && (!mouse.isMouse2()) - && (!mouse.isMouse3()) - && (!mouse.isMouseWheelUp()) - && (!mouse.isMouseWheelDown()) - ) { - break; + // Do not switch if a window is modal + if (activeWindow.isModal()) { + return; + } + + int nextWindowI = activeWindowI; + for (;;) { + if (forward) { + nextWindowI++; + nextWindowI %= windows.size(); + } else { + nextWindowI--; + if (nextWindowI < 0) { + nextWindowI = windows.size() - 1; } - // We navigated away from a sub-menu, so close it - closeSubMenu(); } - // Convert the mouse relative x/y to menu coordinates - assert (mouse.getX() == mouse.getAbsoluteX()); - assert (mouse.getY() == mouse.getAbsoluteY()); - if (subMenus.size() > 0) { - menu = subMenus.get(subMenus.size() - 1); + if (windows.get(nextWindowI).isShown()) { + activateWindow(windows.get(nextWindowI)); + break; } - mouse.setX(mouse.getX() - menu.getX()); - mouse.setY(mouse.getY() - menu.getY()); } - menu.handleEvent(event); + } // synchronized (windows) + + } + + /** + * Add a window to my window list and make it active. Note package + * private access. + * + * @param window new window to add + */ + final void addWindowToApplication(final TWindow window) { + + // Do not add menu windows to the window list. + if (window instanceof TMenu) { return; } - if (event instanceof TKeypressEvent) { - TKeypressEvent keypress = (TKeypressEvent) event; + // Do not add the desktop to the window list. + if (window instanceof TDesktop) { + return; + } - // See if this key matches an accelerator, and is not being - // shortcutted by the active window, and if so dispatch the menu - // event. - boolean windowWillShortcut = false; - for (TWindow window: windows) { - if (window.isActive()) { - if (window.isShortcutKeypress(keypress.getKey())) { - // We do not process this key, it will be passed to - // the window instead. - windowWillShortcut = true; - } - } + synchronized (windows) { + if (windows.contains(window)) { + throw new IllegalArgumentException("Window " + window + + " is already in window list"); } - if (!windowWillShortcut) { - TKeypress keypressLowercase = keypress.getKey().toLowerCase(); - TMenuItem item = null; - synchronized (accelerators) { - item = accelerators.get(keypressLowercase); + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); } - if (item != null) { - if (item.isEnabled()) { - // Let the menu item dispatch - item.dispatch(); - return; + } + + // Do not allow a modal window to spawn a non-modal window. If a + // modal window is active, then this window will become modal + // too. + if (modalWindowActive()) { + window.flags |= TWindow.MODAL; + window.flags |= TWindow.CENTERED; + window.hidden = false; + } + if (window.isShown()) { + for (TWindow w: windows) { + if (w.isActive()) { + w.setActive(false); + w.onUnfocus(); } + w.setZ(w.getZ() + 1); } } - - // Handle the keypress - if (onKeypress(keypress)) { - return; + windows.add(window); + if (window.isShown()) { + activeWindow = window; + activeWindow.setZ(0); + activeWindow.setActive(true); + activeWindow.onFocus(); } - } - if (event instanceof TCommandEvent) { - if (onCommand((TCommandEvent) event)) { - return; - } - } + if (((window.flags & TWindow.CENTERED) == 0) + && ((window.flags & TWindow.ABSOLUTEXY) == 0) + && (smartWindowPlacement == true) + && (!(window instanceof TDesktop)) + ) { - if (event instanceof TMenuEvent) { - if (onMenu((TMenuEvent) event)) { - return; + doSmartPlacement(window); } } - // Dispatch events to the active window ------------------------------- - for (TWindow window: windows) { - if (window.isActive()) { - if (event instanceof TMouseEvent) { - TMouseEvent mouse = (TMouseEvent) event; - // Convert the mouse relative x/y to window coordinates - assert (mouse.getX() == mouse.getAbsoluteX()); - assert (mouse.getY() == mouse.getAbsoluteY()); - mouse.setX(mouse.getX() - window.getX()); - mouse.setY(mouse.getY() - window.getY()); - } - if (debugEvents) { - System.err.printf("TApplication dispatch event: %s\n", - event); - } - window.handleEvent(event); - break; - } + // Desktop cannot be active over any other window. + if (desktop != null) { + desktop.setActive(false); } } + /** - * Dispatch one event to the appropriate widget or application-level - * event handler. This is the secondary event handler used by certain - * special dialogs (currently TMessageBox and TFileOpenBox). + * Check if there is a system-modal window on top. * - * @param event the input event to consume - * @see #primaryHandleEvent(TInputEvent event) + * @return true if the active window is modal */ - private void secondaryHandleEvent(final TInputEvent event) { - secondaryEventReceiver.handleEvent(event); + private boolean modalWindowActive() { + if (windows.size() == 0) { + return false; + } + + for (TWindow w: windows) { + if (w.isModal()) { + return true; + } + } + + return false; } /** - * Enable a widget to override the primary event thread. + * Check if there is a window with overridden menu flag on top. * - * @param widget widget that will receive events + * @return true if the active window is overriding the menu */ - public final void enableSecondaryEventReceiver(final TWidget widget) { - assert (secondaryEventReceiver == null); - assert (secondaryEventHandler == null); - assert ((widget instanceof TMessageBox) - || (widget instanceof TFileOpenBox)); - secondaryEventReceiver = widget; - secondaryEventHandler = new WidgetEventHandler(this, false); - (new Thread(secondaryEventHandler)).start(); + private boolean overrideMenuWindowActive() { + if (activeWindow != null) { + if (activeWindow.hasOverriddenMenu()) { + return true; + } + } + + return false; } /** - * Yield to the secondary thread. + * Close all open windows. */ - 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) { - try { - primaryEventHandler.wait(); - } catch (InterruptedException e) { - // SQUASH - } - } + private void closeAllWindows() { + // Don't do anything if we are in the menu + if (activeMenu != null) { + return; + } + while (windows.size() > 0) { + closeWindow(windows.get(0)); } } /** - * Do stuff when there is no user input. + * Re-layout the open windows as non-overlapping tiles. This produces + * almost the same results as Turbo Pascal 7.0's IDE. */ - 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()) { - timer.tick(); - if (timer.recurring) { - keepTimers.add(timer); + private void tileWindows() { + synchronized (windows) { + // Don't do anything if we are in the menu + if (activeMenu != null) { + return; + } + int z = windows.size(); + if (z == 0) { + return; + } + int a = 0; + int b = 0; + a = (int)(Math.sqrt(z)); + int c = 0; + while (c < a) { + b = (z - c) / a; + if (((a * b) + c) == z) { + break; } - } else { - keepTimers.add(timer); + c++; } - } - timers = keepTimers; + assert (a > 0); + assert (b > 0); + assert (c < a); + int newWidth = (getScreen().getWidth() / a); + int newHeight1 = ((getScreen().getHeight() - 1) / b); + int newHeight2 = ((getScreen().getHeight() - 1) / (b + c)); - // Call onIdle's - for (TWindow window: windows) { - window.onIdle(); + List sorted = new ArrayList(windows); + Collections.sort(sorted); + Collections.reverse(sorted); + for (int i = 0; i < sorted.size(); i++) { + int logicalX = i / b; + int logicalY = i % b; + if (i >= ((a - 1) * b)) { + logicalX = a - 1; + logicalY = i - ((a - 1) * b); + } + + TWindow w = sorted.get(i); + int oldWidth = w.getWidth(); + int oldHeight = w.getHeight(); + + w.setX(logicalX * newWidth); + w.setWidth(newWidth); + if (i >= ((a - 1) * b)) { + w.setY((logicalY * newHeight2) + 1); + w.setHeight(newHeight2); + } else { + w.setY((logicalY * newHeight1) + 1); + w.setHeight(newHeight1); + } + if ((w.getWidth() != oldWidth) + || (w.getHeight() != oldHeight) + ) { + w.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, + w.getWidth(), w.getHeight())); + } + } } } /** - * Get the amount of time I can sleep before missing a Timer tick. - * - * @param timeout = initial (maximum) timeout in millis - * @return number of milliseconds between now and the next timer event + * Re-layout the open windows as overlapping cascaded windows. */ - private long getSleepTime(final long timeout) { - 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; + private void cascadeWindows() { + synchronized (windows) { + // Don't do anything if we are in the menu + if (activeMenu != null) { + return; } - - long timeDifference = nextTickTime - nowTime; - if (timeDifference < sleepTime) { - sleepTime = timeDifference; + int x = 0; + int y = 1; + List sorted = new ArrayList(windows); + Collections.sort(sorted); + Collections.reverse(sorted); + for (TWindow window: sorted) { + window.setX(x); + window.setY(y); + x++; + y++; + if (x > getScreen().getWidth()) { + x = 0; + } + if (y >= getScreen().getHeight()) { + y = 1; + } } } - assert (sleepTime >= 0); - assert (sleepTime <= timeout); - return sleepTime; } /** - * Close window. Note that the window's destructor is NOT called by this - * method, instead the GC is assumed to do the cleanup. + * Place a window to minimize its overlap with other windows. * - * @param window the window to remove + * @param window the window to place */ - public final void closeWindow(final TWindow window) { - synchronized (windows) { - int z = window.getZ(); - window.setZ(-1); - window.onUnfocus(); - Collections.sort(windows); - windows.remove(0); - TWindow activeWindow = null; - for (TWindow w: windows) { - if (w.getZ() > z) { - w.setZ(w.getZ() - 1); - if (w.getZ() == 0) { - w.setActive(true); - w.onFocus(); - assert (activeWindow == null); - activeWindow = w; - } else { - if (w.isActive()) { - w.setActive(false); - w.onUnfocus(); - } - } - } - } + public final void doSmartPlacement(final TWindow window) { + // This is a pretty dumb algorithm, but seems to work. The hardest + // part is computing these "overlap" values seeking a minimum average + // overlap. + int xMin = 0; + int yMin = desktopTop; + int xMax = getScreen().getWidth() - window.getWidth() + 1; + int yMax = desktopBottom - window.getHeight() + 1; + if (xMax < xMin) { + xMax = xMin; + } + if (yMax < yMin) { + yMax = yMin; } - // Perform window cleanup - window.onClose(); - - // Check if we are closing a TMessageBox or similar - if (secondaryEventReceiver != null) { - assert (secondaryEventHandler != null); - - // Do not send events to the secondaryEventReceiver anymore, the - // window is closed. - secondaryEventReceiver = null; + if ((xMin == xMax) && (yMin == yMax)) { + // No work to do, bail out. + return; + } - // Wake the secondary thread, it will wake the primary as it - // exits. - synchronized (secondaryEventHandler) { - secondaryEventHandler.notify(); + // Compute the overlap matrix without the new window. + int width = getScreen().getWidth(); + int height = getScreen().getHeight(); + int overlapMatrix[][] = new int[width][height]; + for (TWindow w: windows) { + if (window == w) { + continue; + } + for (int x = w.getX(); x < w.getX() + w.getWidth(); x++) { + if (x < 0) { + continue; + } + if (x >= width) { + continue; + } + for (int y = w.getY(); y < w.getY() + w.getHeight(); y++) { + if (y < 0) { + continue; + } + if (y >= height) { + continue; + } + overlapMatrix[x][y]++; + } } } - } - /** - * Switch to the next window. - * - * @param forward if true, then switch to the next window in the list, - * otherwise switch to the previous window in the list - */ - public final void switchWindow(final boolean forward) { - // Only switch if there are multiple windows - if (windows.size() < 2) { - return; + long oldOverlapTotal = 0; + long oldOverlapN = 0; + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + oldOverlapTotal += overlapMatrix[x][y]; + if (overlapMatrix[x][y] > 0) { + oldOverlapN++; + } + } } - synchronized (windows) { - // Swap z/active between active window and the next in the list - int activeWindowI = -1; - for (int i = 0; i < windows.size(); i++) { - if (windows.get(i).isActive()) { - activeWindowI = i; - break; + double oldOverlapAvg = (double) oldOverlapTotal / (double) oldOverlapN; + boolean first = true; + int windowX = window.getX(); + int windowY = window.getY(); + + // For each possible (x, y) position for the new window, compute a + // new overlap matrix. + for (int x = xMin; x < xMax; x++) { + for (int y = yMin; y < yMax; y++) { + + // Start with the matrix minus this window. + int newMatrix[][] = new int[width][height]; + for (int mx = 0; mx < width; mx++) { + for (int my = 0; my < height; my++) { + newMatrix[mx][my] = overlapMatrix[mx][my]; + } } - } - assert (activeWindowI >= 0); - // Do not switch if a window is modal - if (windows.get(activeWindowI).isModal()) { - return; - } + // Add this window's values to the new overlap matrix. + long newOverlapTotal = 0; + long newOverlapN = 0; + // Start by adding each new cell. + for (int wx = x; wx < x + window.getWidth(); wx++) { + if (wx >= width) { + continue; + } + for (int wy = y; wy < y + window.getHeight(); wy++) { + if (wy >= height) { + continue; + } + newMatrix[wx][wy]++; + } + } + // Now figure out the new value for total coverage. + for (int mx = 0; mx < width; mx++) { + for (int my = 0; my < height; my++) { + newOverlapTotal += newMatrix[x][y]; + if (newMatrix[mx][my] > 0) { + newOverlapN++; + } + } + } + double newOverlapAvg = (double) newOverlapTotal / (double) newOverlapN; - int nextWindowI; - if (forward) { - nextWindowI = (activeWindowI + 1) % windows.size(); - } else { - if (activeWindowI == 0) { - nextWindowI = windows.size() - 1; + if (first) { + // First time: just record what we got. + oldOverlapAvg = newOverlapAvg; + first = false; } else { - nextWindowI = activeWindowI - 1; + // All other times: pick a new best (x, y) and save the + // overlap value. + if (newOverlapAvg < oldOverlapAvg) { + windowX = x; + windowY = y; + oldOverlapAvg = newOverlapAvg; + } } - } - windows.get(activeWindowI).setActive(false); - windows.get(activeWindowI).setZ(windows.get(nextWindowI).getZ()); - windows.get(activeWindowI).onUnfocus(); - windows.get(nextWindowI).setZ(0); - windows.get(nextWindowI).setActive(true); - windows.get(nextWindowI).onFocus(); - } // synchronized (windows) + } // for (int x = xMin; x < xMax; x++) - } + } // for (int y = yMin; y < yMax; y++) - /** - * Add a window to my window list and make it active. - * - * @param window new window to add - */ - public final void addWindow(final TWindow window) { - synchronized (windows) { - // Do not allow a modal window to spawn a non-modal window - if ((windows.size() > 0) && (windows.get(0).isModal())) { - assert (window.isModal()); - } - for (TWindow w: windows) { - if (w.isActive()) { - w.setActive(false); - w.onUnfocus(); - } - w.setZ(w.getZ() + 1); - } - windows.add(window); - window.setZ(0); - window.setActive(true); - window.onFocus(); - } + // Finally, set the window's new coordinates. + window.setX(windowX); + window.setY(windowY); } - /** - * Check if there is a system-modal window on top. - * - * @return true if the active window is modal - */ - private boolean modalWindowActive() { - if (windows.size() == 0) { - return false; - } - return windows.get(windows.size() - 1).isModal(); - } + // ------------------------------------------------------------------------ + // TMenu management ------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Check if a mouse event would hit either the active menu or any open @@ -1285,7 +2802,7 @@ public class TApplication implements Runnable { */ private boolean mouseOnMenu(final TMouseEvent mouse) { assert (activeMenu != null); - List menus = new LinkedList(subMenus); + List menus = new ArrayList(subMenus); Collections.reverse(menus); for (TMenu menu: menus) { if (menu.mouseWouldHit(mouse)) { @@ -1322,7 +2839,9 @@ public class TApplication implements Runnable { if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) && (mouse.isMouse1()) && (!modalWindowActive()) + && (!overrideMenuWindowActive()) && (mouse.getAbsoluteY() == 0) + && (hideMenuBar == false) ) { for (TMenu menu: subMenus) { @@ -1332,9 +2851,9 @@ public class TApplication implements Runnable { // They selected the menu, go activate it for (TMenu menu: menus) { - if ((mouse.getAbsoluteX() >= menu.getX()) - && (mouse.getAbsoluteX() < menu.getX() - + menu.getTitle().length() + 2) + if ((mouse.getAbsoluteX() >= menu.getTitleX()) + && (mouse.getAbsoluteX() < menu.getTitleX() + + StringUtils.width(menu.getTitle()) + 2) ) { menu.setActive(true); activeMenu = menu; @@ -1350,6 +2869,7 @@ public class TApplication implements Runnable { && (mouse.isMouse1()) && (activeMenu != null) && (mouse.getAbsoluteY() == 0) + && (hideMenuBar == false) ) { TMenu oldMenu = activeMenu; @@ -1360,9 +2880,9 @@ public class TApplication implements Runnable { // See if we should switch menus for (TMenu menu: menus) { - if ((mouse.getAbsoluteX() >= menu.getX()) - && (mouse.getAbsoluteX() < menu.getX() - + menu.getTitle().length() + 2) + if ((mouse.getAbsoluteX() >= menu.getTitleX()) + && (mouse.getAbsoluteX() < menu.getTitleX() + + StringUtils.width(menu.getTitle()) + 2) ) { menu.setActive(true); activeMenu = menu; @@ -1375,46 +2895,66 @@ public class TApplication implements Runnable { return; } - // Only switch if there are multiple windows - if (windows.size() < 2) { + // If a menu is still active, don't switch windows + if (activeMenu != null) { return; } - // Switch on the upclick - if (mouse.getType() != TMouseEvent.Type.MOUSE_UP) { + // Only switch if there are multiple windows + if (windows.size() < 2) { return; } - synchronized (windows) { - Collections.sort(windows); - if (windows.get(0).isModal()) { - // Modal windows don't switch - return; - } + if (((focusFollowsMouse == true) + && (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)) + || (mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) + ) { + synchronized (windows) { + Collections.sort(windows); + if (windows.get(0).isModal()) { + // Modal windows don't switch + return; + } - for (TWindow window: windows) { - assert (!window.isModal()); - if (window.mouseWouldHit(mouse)) { - if (window == windows.get(0)) { - // Clicked on the same window, nothing to do - return; + for (TWindow window: windows) { + assert (!window.isModal()); + + if (window.isHidden()) { + assert (!window.isActive()); + continue; } - // We will be switching to another window - assert (windows.get(0).isActive()); - assert (!window.isActive()); - windows.get(0).onUnfocus(); - windows.get(0).setActive(false); - windows.get(0).setZ(window.getZ()); - window.setZ(0); - window.setActive(true); - window.onFocus(); - return; + if (window.mouseWouldHit(mouse)) { + if (window == windows.get(0)) { + // Clicked on the same window, nothing to do + assert (window.isActive()); + return; + } + + // We will be switching to another window + assert (windows.get(0).isActive()); + assert (windows.get(0) == activeWindow); + assert (!window.isActive()); + if (activeWindow != null) { + activeWindow.onUnfocus(); + activeWindow.setActive(false); + activeWindow.setZ(window.getZ()); + } + activeWindow = window; + window.setZ(0); + window.setActive(true); + window.onFocus(); + return; + } } } + + // Clicked on the background, nothing to do + return; } - // Clicked on the background, nothing to do + // Nothing to do: this isn't a mouse up, or focus isn't following + // mouse. return; } @@ -1432,6 +2972,53 @@ public class TApplication implements Runnable { } } + /** + * Get a (shallow) copy of the menu list. + * + * @return a copy of the menu list + */ + public final List getAllMenus() { + return new ArrayList(menus); + } + + /** + * Add a top-level menu to the list. + * + * @param menu the menu to add + * @throws IllegalArgumentException if the menu is already used in + * another TApplication + */ + public final void addMenu(final TMenu menu) { + if ((menu.getApplication() != null) + && (menu.getApplication() != this) + ) { + throw new IllegalArgumentException("Menu " + menu + " is already " + + "part of application " + menu.getApplication()); + } + closeMenu(); + menus.add(menu); + recomputeMenuX(); + } + + /** + * Remove a top-level menu from the list. + * + * @param menu the menu to remove + * @throws IllegalArgumentException if the menu is already used in + * another TApplication + */ + public final void removeMenu(final TMenu menu) { + if ((menu.getApplication() != null) + && (menu.getApplication() != this) + ) { + throw new IllegalArgumentException("Menu " + menu + " is already " + + "part of application " + menu.getApplication()); + } + closeMenu(); + menus.remove(menu); + recomputeMenuX(); + } + /** * Turn off a sub-menu. */ @@ -1451,6 +3038,7 @@ public class TApplication implements Runnable { */ public final void switchMenu(final boolean forward) { assert (activeMenu != null); + assert (hideMenuBar == false); for (TMenu menu: subMenus) { menu.setActive(false); @@ -1462,10 +3050,14 @@ public class TApplication implements Runnable { if (forward) { if (i < menus.size() - 1) { i++; + } else { + i = 0; } } else { if (i > 0) { i--; + } else { + i = menus.size() - 1; } } activeMenu.setActive(false); @@ -1476,114 +3068,6 @@ public class TApplication implements Runnable { } } - /** - * Method that TApplication subclasses can override to handle menu or - * posted command events. - * - * @param command command event - * @return if true, this event was consumed - */ - protected boolean onCommand(final TCommandEvent command) { - // Default: handle cmExit - if (command.equals(cmExit)) { - if (messageBox("Confirmation", "Exit application?", - TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { - quit = true; - } - return true; - } - - if (command.equals(cmShell)) { - openTerminal(0, 0, TWindow.RESIZABLE); - return true; - } - - if (command.equals(cmTile)) { - tileWindows(); - return true; - } - if (command.equals(cmCascade)) { - cascadeWindows(); - return true; - } - if (command.equals(cmCloseAll)) { - closeAllWindows(); - return true; - } - - return false; - } - - /** - * Method that TApplication subclasses can override to handle menu - * events. - * - * @param menu menu event - * @return if true, this event was consumed - */ - protected boolean onMenu(final TMenuEvent menu) { - - // Default: handle MID_EXIT - if (menu.getId() == TMenu.MID_EXIT) { - if (messageBox("Confirmation", "Exit application?", - TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { - quit = true; - } - return true; - } - - if (menu.getId() == TMenu.MID_SHELL) { - openTerminal(0, 0, TWindow.RESIZABLE); - return true; - } - - if (menu.getId() == TMenu.MID_TILE) { - tileWindows(); - return true; - } - if (menu.getId() == TMenu.MID_CASCADE) { - cascadeWindows(); - return true; - } - if (menu.getId() == TMenu.MID_CLOSE_ALL) { - closeAllWindows(); - return true; - } - return false; - } - - /** - * Method that TApplication subclasses can override to handle keystrokes. - * - * @param keypress keystroke event - * @return if true, this event was consumed - */ - protected boolean onKeypress(final TKeypressEvent keypress) { - // Default: only menu shortcuts - - // Process Alt-F, Alt-E, etc. menu shortcut keys - if (!keypress.getKey().isFnKey() - && keypress.getKey().isAlt() - && !keypress.getKey().isCtrl() - && (activeMenu == null) - ) { - - assert (subMenus.size() == 0); - - for (TMenu menu: menus) { - if (Character.toLowerCase(menu.getMnemonic().getShortcut()) - == Character.toLowerCase(keypress.getKey().getChar()) - ) { - activeMenu = menu; - menu.setActive(true); - return true; - } - } - } - - return false; - } - /** * Add a menu item to the global list. If it has a keyboard accelerator, * that will be added the global hash. @@ -1626,6 +3110,7 @@ public class TApplication implements Runnable { for (TMenuItem item: menuItems) { if ((item.getId() >= lower) && (item.getId() <= upper)) { item.setEnabled(false); + item.getParent().activate(0); } } } @@ -1639,6 +3124,7 @@ public class TApplication implements Runnable { for (TMenuItem item: menuItems) { if (item.getId() == id) { item.setEnabled(true); + item.getParent().activate(0); } } } @@ -1654,8 +3140,24 @@ public class TApplication implements Runnable { for (TMenuItem item: menuItems) { if ((item.getId() >= lower) && (item.getId() <= upper)) { item.setEnabled(true); + item.getParent().activate(0); + } + } + } + + /** + * Get the menu item associated with this ID. + * + * @param id the menu item ID + * @return the menu item, or null if not found + */ + public final TMenuItem getMenuItem(final int id) { + for (TMenuItem item: menuItems) { + if (item.getId() == id) { + return item; } } + return null; } /** @@ -1665,7 +3167,32 @@ public class TApplication implements Runnable { int x = 0; for (TMenu menu: menus) { menu.setX(x); - x += menu.getTitle().length() + 2; + menu.setTitleX(x); + x += StringUtils.width(menu.getTitle()) + 2; + + // Don't let the menu window exceed the screen width + int rightEdge = menu.getX() + menu.getWidth(); + if (rightEdge > getScreen().getWidth()) { + menu.setX(getScreen().getWidth() - menu.getWidth()); + } + } + } + + /** + * Post an event to process. + * + * @param event new event to add to the queue + */ + public final void postEvent(final TInputEvent event) { + synchronized (this) { + synchronized (fillEventQueue) { + fillEventQueue.add(event); + } + if (debugThreads) { + System.err.println(System.currentTimeMillis() + " " + + Thread.currentThread() + " postEvent() wake up main"); + } + this.notify(); } } @@ -1675,10 +3202,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(); } /** @@ -1705,17 +3239,35 @@ public class TApplication implements Runnable { return menu; } + /** + * Convenience function to add a default tools (hamburger) menu. + * + * @return the new menu + */ + public final TMenu addToolMenu() { + TMenu toolMenu = addMenu(i18n.getString("toolMenuTitle")); + toolMenu.addDefaultItem(TMenu.MID_REPAINT); + toolMenu.addDefaultItem(TMenu.MID_VIEW_IMAGE); + toolMenu.addDefaultItem(TMenu.MID_SCREEN_OPTIONS); + TStatusBar toolStatusBar = toolMenu.newStatusBar(i18n. + getString("toolMenuStatus")); + toolStatusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help")); + return toolMenu; + } + /** * Convenience function to add a default "File" menu. * * @return the new menu */ public final TMenu addFileMenu() { - TMenu fileMenu = addMenu("&File"); - fileMenu.addDefaultItem(TMenu.MID_OPEN_FILE); - fileMenu.addSeparator(); + TMenu fileMenu = addMenu(i18n.getString("fileMenuTitle")); fileMenu.addDefaultItem(TMenu.MID_SHELL); + fileMenu.addSeparator(); fileMenu.addDefaultItem(TMenu.MID_EXIT); + TStatusBar statusBar = fileMenu.newStatusBar(i18n. + getString("fileMenuStatus")); + statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help")); return fileMenu; } @@ -1725,11 +3277,14 @@ public class TApplication implements Runnable { * @return the new menu */ public final TMenu addEditMenu() { - TMenu editMenu = addMenu("&Edit"); + TMenu editMenu = addMenu(i18n.getString("editMenuTitle")); editMenu.addDefaultItem(TMenu.MID_CUT); editMenu.addDefaultItem(TMenu.MID_COPY); editMenu.addDefaultItem(TMenu.MID_PASTE); editMenu.addDefaultItem(TMenu.MID_CLEAR); + TStatusBar statusBar = editMenu.newStatusBar(i18n. + getString("editMenuStatus")); + statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help")); return editMenu; } @@ -1739,7 +3294,7 @@ public class TApplication implements Runnable { * @return the new menu */ public final TMenu addWindowMenu() { - TMenu windowMenu = addMenu("&Window"); + TMenu windowMenu = addMenu(i18n.getString("windowMenuTitle")); windowMenu.addDefaultItem(TMenu.MID_TILE); windowMenu.addDefaultItem(TMenu.MID_CASCADE); windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL); @@ -1749,106 +3304,123 @@ public class TApplication implements Runnable { windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT); windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS); windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE); + TStatusBar statusBar = windowMenu.newStatusBar(i18n. + getString("windowMenuStatus")); + statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help")); return windowMenu; } /** - * Close all open windows. + * Convenience function to add a default "Help" menu. + * + * @return the new menu + */ + public final TMenu addHelpMenu() { + TMenu helpMenu = addMenu(i18n.getString("helpMenuTitle")); + helpMenu.addDefaultItem(TMenu.MID_HELP_CONTENTS); + helpMenu.addDefaultItem(TMenu.MID_HELP_INDEX); + helpMenu.addDefaultItem(TMenu.MID_HELP_SEARCH); + helpMenu.addDefaultItem(TMenu.MID_HELP_PREVIOUS); + helpMenu.addDefaultItem(TMenu.MID_HELP_HELP); + helpMenu.addDefaultItem(TMenu.MID_HELP_ACTIVE_FILE); + helpMenu.addSeparator(); + helpMenu.addDefaultItem(TMenu.MID_ABOUT); + TStatusBar statusBar = helpMenu.newStatusBar(i18n. + getString("helpMenuStatus")); + statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help")); + return helpMenu; + } + + /** + * Convenience function to add a default "Table" menu. + * + * @return the new menu */ - private void closeAllWindows() { - // Don't do anything if we are in the menu - if (activeMenu != null) { - return; - } - while (windows.size() > 0) { - closeWindow(windows.get(0)); - } - } + public final TMenu addTableMenu() { + TMenu tableMenu = addMenu(i18n.getString("tableMenuTitle")); + tableMenu.addDefaultItem(TMenu.MID_TABLE_RENAME_COLUMN, false); + tableMenu.addDefaultItem(TMenu.MID_TABLE_RENAME_ROW, false); + tableMenu.addSeparator(); + + TSubMenu viewMenu = tableMenu.addSubMenu(i18n. + getString("tableSubMenuView")); + viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_ROW_LABELS, false); + viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_COLUMN_LABELS, false); + viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW, false); + viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN, false); + + TSubMenu borderMenu = tableMenu.addSubMenu(i18n. + getString("tableSubMenuBorders")); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_NONE, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_ALL, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_CELL_NONE, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_CELL_ALL, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_RIGHT, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_LEFT, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_TOP, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_BOTTOM, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_DOUBLE_BOTTOM, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_THICK_BOTTOM, false); + TSubMenu deleteMenu = tableMenu.addSubMenu(i18n. + getString("tableSubMenuDelete")); + deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_LEFT, false); + deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_UP, false); + deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_ROW, false); + deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_COLUMN, false); + TSubMenu insertMenu = tableMenu.addSubMenu(i18n. + getString("tableSubMenuInsert")); + insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_LEFT, false); + insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_RIGHT, false); + insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_ABOVE, false); + insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_BELOW, false); + TSubMenu columnMenu = tableMenu.addSubMenu(i18n. + getString("tableSubMenuColumn")); + columnMenu.addDefaultItem(TMenu.MID_TABLE_COLUMN_NARROW, false); + columnMenu.addDefaultItem(TMenu.MID_TABLE_COLUMN_WIDEN, false); + TSubMenu fileMenu = tableMenu.addSubMenu(i18n. + getString("tableSubMenuFile")); + fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_OPEN_CSV, false); + fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_SAVE_CSV, false); + fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_SAVE_TEXT, false); + + TStatusBar statusBar = tableMenu.newStatusBar(i18n. + getString("tableMenuStatus")); + statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help")); + return tableMenu; + } + + // ------------------------------------------------------------------------ + // TTimer management ------------------------------------------------------ + // ------------------------------------------------------------------------ /** - * Re-layout the open windows as non-overlapping tiles. This produces - * almost the same results as Turbo Pascal 7.0's IDE. + * Get the amount of time I can sleep before missing a Timer tick. + * + * @param timeout = initial (maximum) timeout in millis + * @return number of milliseconds between now and the next timer event */ - private void tileWindows() { - synchronized (windows) { - // Don't do anything if we are in the menu - if (activeMenu != null) { - return; - } - int z = windows.size(); - if (z == 0) { - return; - } - int a = 0; - int b = 0; - a = (int)(Math.sqrt(z)); - int c = 0; - while (c < a) { - b = (z - c) / a; - if (((a * b) + c) == z) { - break; - } - c++; - } - assert (a > 0); - assert (b > 0); - assert (c < a); - int newWidth = (getScreen().getWidth() / a); - int newHeight1 = ((getScreen().getHeight() - 1) / b); - int newHeight2 = ((getScreen().getHeight() - 1) / (b + c)); + private long getSleepTime(final long timeout) { + Date now = new Date(); + long nowTime = now.getTime(); + long sleepTime = timeout; - List sorted = new LinkedList(windows); - Collections.sort(sorted); - Collections.reverse(sorted); - for (int i = 0; i < sorted.size(); i++) { - int logicalX = i / b; - int logicalY = i % b; - if (i >= ((a - 1) * b)) { - logicalX = a - 1; - logicalY = i - ((a - 1) * b); + synchronized (timers) { + for (TTimer timer: timers) { + long nextTickTime = timer.getNextTick().getTime(); + if (nextTickTime < nowTime) { + return 0; } - TWindow w = sorted.get(i); - w.setX(logicalX * newWidth); - w.setWidth(newWidth); - if (i >= ((a - 1) * b)) { - w.setY((logicalY * newHeight2) + 1); - w.setHeight(newHeight2); - } else { - w.setY((logicalY * newHeight1) + 1); - w.setHeight(newHeight1); + long timeDifference = nextTickTime - nowTime; + if (timeDifference < sleepTime) { + sleepTime = timeDifference; } } } - } - /** - * Re-layout the open windows as overlapping cascaded windows. - */ - private void cascadeWindows() { - synchronized (windows) { - // Don't do anything if we are in the menu - if (activeMenu != null) { - return; - } - int x = 0; - int y = 1; - List sorted = new LinkedList(windows); - Collections.sort(sorted); - Collections.reverse(sorted); - for (TWindow window: sorted) { - window.setX(x); - window.setY(y); - x++; - y++; - if (x > getScreen().getWidth()) { - x = 0; - } - if (y >= getScreen().getHeight()) { - y = 1; - } - } - } + assert (sleepTime >= 0); + assert (sleepTime <= timeout); + return sleepTime; } /** @@ -1880,6 +3452,10 @@ public class TApplication implements Runnable { } } + // ------------------------------------------------------------------------ + // Other TWindow constructors --------------------------------------------- + // ------------------------------------------------------------------------ + /** * Convenience function to spawn a message box. * @@ -1938,6 +3514,22 @@ public class TApplication implements Runnable { return new TInputBox(this, title, caption, text); } + /** + * 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 + * @param type one of the Type constants. Default is Type.OK. + * @return the new input box + */ + public final TInputBox inputBox(final String title, final String caption, + final String text, final TInputBox.Type type) { + + return new TInputBox(this, title, caption, text, type); + } + /** * Convenience function to open a terminal window. * @@ -1949,6 +3541,20 @@ public class TApplication implements Runnable { 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 closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final boolean closeOnExit) { + + return openTerminal(x, y, TWindow.RESIZABLE, closeOnExit); + } + /** * Convenience function to open a terminal window. * @@ -1963,6 +3569,119 @@ public class TApplication implements Runnable { return new TTerminalWindow(this, x, y, flags); } + /** + * 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 + * @param closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final int flags, final boolean closeOnExit) { + + return new TTerminalWindow(this, x, y, flags, closeOnExit); + } + + /** + * Convenience function to open a terminal window and execute a custom + * command line inside it. + * + * @param x column relative to parent + * @param y row relative to parent + * @param commandLine the command line to execute + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final String commandLine) { + + return openTerminal(x, y, TWindow.RESIZABLE, commandLine); + } + + /** + * Convenience function to open a terminal window and execute a custom + * command line inside it. + * + * @param x column relative to parent + * @param y row relative to parent + * @param commandLine the command line to execute + * @param closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final String commandLine, final boolean closeOnExit) { + + return openTerminal(x, y, TWindow.RESIZABLE, commandLine, closeOnExit); + } + + /** + * Convenience function to open a terminal window and execute a custom + * command line inside it. + * + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param command the command line to execute + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final int flags, final String [] command) { + + return new TTerminalWindow(this, x, y, flags, command); + } + + /** + * Convenience function to open a terminal window and execute a custom + * command line inside it. + * + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param command the command line to execute + * @param closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final int flags, final String [] command, final boolean closeOnExit) { + + return new TTerminalWindow(this, x, y, flags, command, closeOnExit); + } + + /** + * Convenience function to open a terminal window and execute a custom + * command line inside it. + * + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param commandLine the command line to execute + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final int flags, final String commandLine) { + + return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s+")); + } + + /** + * Convenience function to open a terminal window and execute a custom + * command line inside it. + * + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param commandLine the command line to execute + * @param closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final int flags, final String commandLine, final boolean closeOnExit) { + + return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s+"), + closeOnExit); + } + /** * Convenience function to spawn an file open box. * @@ -1991,4 +3710,109 @@ public class TApplication implements Runnable { return box.getFilename(); } + /** + * Convenience function to spawn a file open box. + * + * @param path path of selected file + * @param type one of the Type constants + * @param filter a string that files must match to be displayed + * @return the result of the new file open box + * @throws IOException of a java.io operation throws + */ + public final String fileOpenBox(final String path, + final TFileOpenBox.Type type, final String filter) throws IOException { + + ArrayList filters = new ArrayList(); + filters.add(filter); + + TFileOpenBox box = new TFileOpenBox(this, path, type, filters); + return box.getFilename(); + } + + /** + * Convenience function to spawn a file open box. + * + * @param path path of selected file + * @param type one of the Type constants + * @param filters a list of strings that files must match to be displayed + * @return the result of the new file open box + * @throws IOException of a java.io operation throws + */ + public final String fileOpenBox(final String path, + final TFileOpenBox.Type type, + final List filters) throws IOException { + + TFileOpenBox box = new TFileOpenBox(this, path, type, filters); + return box.getFilename(); + } + + /** + * Convenience function to create a new window and make it active. + * Window will be located at (0, 0). + * + * @param title window title, will be centered along the top border + * @param width width of window + * @param height height of window + * @return the new window + */ + public final TWindow addWindow(final String title, final int width, + final int height) { + + TWindow window = new TWindow(this, title, 0, 0, width, height); + return window; + } + + /** + * Convenience function to create a new window and make it active. + * Window will be located at (0, 0). + * + * @param title window title, will be centered along the top border + * @param width width of window + * @param height height of window + * @param flags bitmask of RESIZABLE, CENTERED, or MODAL + * @return the new window + */ + public final TWindow addWindow(final String title, + final int width, final int height, final int flags) { + + TWindow window = new TWindow(this, title, 0, 0, width, height, flags); + return window; + } + + /** + * Convenience function to create a new window and make it active. + * + * @param title window title, will be centered along the top border + * @param x column relative to parent + * @param y row relative to parent + * @param width width of window + * @param height height of window + * @return the new window + */ + public final TWindow addWindow(final String title, + final int x, final int y, final int width, final int height) { + + TWindow window = new TWindow(this, title, x, y, width, height); + return window; + } + + /** + * Convenience function to create a new window and make it active. + * + * @param title window title, will be centered along the top border + * @param x column relative to parent + * @param y row relative to parent + * @param width width of window + * @param height height of window + * @param flags mask of RESIZABLE, CENTERED, or MODAL + * @return the new window + */ + public final TWindow addWindow(final String title, + final int x, final int y, final int width, final int height, + final int flags) { + + TWindow window = new TWindow(this, title, x, y, width, height, flags); + return window; + } + }