X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTApplication.java;h=6dd503f547a141d1a05f0511652c14e038a25f42;hb=d36057dfab8def933a64be042b039d76708ac5ba;hp=e61cea28d631afba3c84964c7a2a13803c597aca;hpb=eb29bbb5ec70c43895dd0f053630c7e3cd402cba;p=fanfix.git diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index e61cea2..6dd503f 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -77,7 +77,7 @@ public class TApplication implements Runnable { private static final ResourceBundle i18n = ResourceBundle.getBundle(TApplication.class.getName()); // ------------------------------------------------------------------------ - // Public constants ------------------------------------------------------- + // Constants -------------------------------------------------------------- // ------------------------------------------------------------------------ /** @@ -117,9 +117,155 @@ public class TApplication implements Runnable { } // ------------------------------------------------------------------------ - // Primary/secondary event handlers --------------------------------------- + // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * The primary event handler thread. + */ + private volatile WidgetEventHandler primaryEventHandler; + + /** + * The secondary event handler thread. + */ + private volatile WidgetEventHandler secondaryEventHandler; + + /** + * The widget receiving events from the secondary event handler thread. + */ + private volatile TWidget secondaryEventReceiver; + + /** + * Access to the physical screen, keyboard, and mouse. + */ + private Backend backend; + + /** + * Actual mouse coordinate X. + */ + private int mouseX; + + /** + * Actual mouse coordinate Y. + */ + private int mouseY; + + /** + * Old version of mouse coordinate X. + */ + private int oldMouseX; + + /** + * Old version mouse coordinate Y. + */ + private int oldMouseY; + + /** + * The last mouse up click time, used to determine if this is a mouse + * double-click. + */ + private long lastMouseUpTime; + + /** + * The amount of millis between mouse up events to assume a double-click. + */ + private long doubleClickTime = 250; + + /** + * Event queue that is filled by run(). + */ + private List fillEventQueue; + + /** + * Event queue that will be drained by either primary or secondary + * Thread. + */ + private List drainEventQueue; + + /** + * Top-level menus in this application. + */ + private List menus; + + /** + * Stack of activated sub-menus in this application. + */ + private List subMenus; + + /** + * The currently active menu. + */ + private TMenu activeMenu = null; + + /** + * Active keyboard accelerators. + */ + private Map accelerators; + + /** + * All menu items. + */ + private List menuItems; + + /** + * Windows and widgets pull colors from this ColorTheme. + */ + private ColorTheme theme; + + /** + * The top-level windows (but not menus). + */ + private List windows; + + /** + * The currently acive window. + */ + 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. + */ + private volatile boolean quit = false; + + /** + * When true, repaint the entire screen. + */ + private volatile boolean repaint = true; + + /** + * Y coordinate of the top edge of the desktop. For now this is a + * constant. Someday it would be nice to have a multi-line menu or + * toolbars. + */ + private static final int desktopTop = 1; + + /** + * Y coordinate of the bottom edge of the desktop. + */ + private int desktopBottom; + + /** + * An optional TDesktop background window that is drawn underneath + * everything else. + */ + private TDesktop desktop; + + /** + * If true, focus follows mouse: windows automatically raised if the + * mouse passes over them. + */ + private boolean focusFollowsMouse = false; + /** * WidgetEventHandler is the main event consumer loop. There are at most * two such threads in existence: the primary for normal case and a @@ -264,1163 +410,1153 @@ public class TApplication implements Runnable { } } - /** - * The primary event handler thread. - */ - private volatile WidgetEventHandler primaryEventHandler; + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** - * The secondary event handler thread. + * 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 */ - private volatile WidgetEventHandler secondaryEventHandler; + public TApplication(final BackendType backendType, final int windowWidth, + final int windowHeight, final int fontSize) + throws UnsupportedEncodingException { - /** - * The widget receiving events from the secondary event handler thread. - */ - private volatile TWidget secondaryEventReceiver; + 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(); + } /** - * Wake the sleeping active event handler. + * Public constructor. + * + * @param backendType BackendType.XTERM, BackendType.ECMA48 or + * BackendType.SWING + * @throws UnsupportedEncodingException if an exception is thrown when + * creating the InputStreamReader */ - private void wakeEventHandler() { - if (!started) { - return; - } + public TApplication(final BackendType backendType) + throws UnsupportedEncodingException { - if (secondaryEventHandler != null) { - synchronized (secondaryEventHandler) { - secondaryEventHandler.notify(); - } - } else { - assert (primaryEventHandler != null); - synchronized (primaryEventHandler) { - primaryEventHandler.notify(); - } + 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(); } - // ------------------------------------------------------------------------ - // TApplication attributes ------------------------------------------------ - // ------------------------------------------------------------------------ - /** - * Access to the physical screen, keyboard, and mouse. - */ - private Backend backend; - - /** - * Get the Backend. + * Public constructor. The backend type will be BackendType.ECMA48. * - * @return the Backend + * @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 final Backend getBackend() { - return backend; + public TApplication(final InputStream input, + final OutputStream output) throws UnsupportedEncodingException { + + backend = new ECMA48Backend(this, input, output); + TApplicationImpl(); } /** - * Get the Screen. + * Public constructor. The backend type will be BackendType.ECMA48. * - * @return the Screen + * @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. + * @param setRawMode if true, set System.in into raw mode with stty. + * This should in general not be used. It is here solely for Demo3, + * which uses System.in. + * @throws IllegalArgumentException if input, reader, or writer are null. */ - 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(); - } + public TApplication(final InputStream input, final Reader reader, + final PrintWriter writer, final boolean setRawMode) { + + backend = new ECMA48Backend(this, input, reader, writer, setRawMode); + TApplicationImpl(); } /** - * Actual mouse coordinate X. + * 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. */ - private int mouseX; + public TApplication(final InputStream input, final Reader reader, + final PrintWriter writer) { - /** - * Actual mouse coordinate Y. - */ - private int mouseY; + this(input, reader, writer, false); + } /** - * Old version of mouse coordinate X. + * Public constructor. This hook enables use with new non-Jexer + * backends. + * + * @param backend a Backend that is already ready to go. */ - private int oldMouseX; + public TApplication(final Backend backend) { + this.backend = backend; + backend.setListener(this); + TApplicationImpl(); + } /** - * Old version mouse coordinate Y. + * Finish construction once the backend is set. */ - private int oldMouseY; + 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(); + desktop = new TDesktop(this); - /** - * The last mouse up click time, used to determine if this is a mouse - * double-click. - */ - private long lastMouseUpTime; + // 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(); + } + } + ); + } + } + } - /** - * The amount of millis between mouse up events to assume a double-click. - */ - private long doubleClickTime = 250; + // ------------------------------------------------------------------------ + // Runnable --------------------------------------------------------------- + // ------------------------------------------------------------------------ /** - * Event queue that is filled by run(). + * Run this application until it exits. */ - private List fillEventQueue; + public void run() { + // Start the main consumer thread + primaryEventHandler = new WidgetEventHandler(this, true); + (new Thread(primaryEventHandler)).start(); - /** - * Event queue that will be drained by either primary or secondary - * Thread. - */ - private List drainEventQueue; + started = true; - /** - * Top-level menus in this application. - */ - private List menus; + while (!quit) { + synchronized (this) { + boolean doWait = false; - /** - * Stack of activated sub-menus in this application. - */ - private List subMenus; + if (!backend.hasEvents()) { + synchronized (fillEventQueue) { + if (fillEventQueue.size() == 0) { + doWait = true; + } + } + } - /** - * The currently active menu. - */ - private TMenu activeMenu = null; + if (doWait) { + // No I/O to dispatch, so wait until the backend + // provides new I/O. + try { + if (debugThreads) { + System.err.println(System.currentTimeMillis() + + " MAIN sleep"); + } - /** - * Active keyboard accelerators. - */ - private Map accelerators; + this.wait(); - /** - * All menu items. - */ - private List menuItems; + if (debugThreads) { + System.err.println(System.currentTimeMillis() + + " MAIN AWAKE"); + } + } catch (InterruptedException e) { + // I'm awake and don't care why, let's see what's + // going on out there. + } + } - /** - * Windows and widgets pull colors from this ColorTheme. - */ - private ColorTheme theme; + } // synchronized (this) - /** - * Get the color theme. - * - * @return the theme - */ - public final ColorTheme getTheme() { - return theme; - } + synchronized (fillEventQueue) { + // Pull any pending I/O events + backend.getEvents(fillEventQueue); - /** - * The top-level windows (but not menus). - */ - private List windows; + // 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); + } + } - /** - * The currently acive window. - */ - private TWindow activeWindow = null; + // Wake a consumer thread if we have any pending events. + if (drainEventQueue.size() > 0) { + wakeEventHandler(); + } - /** - * Timers that are being ticked. - */ - private List timers; + } // while (!quit) - /** - * When true, the application has been started. - */ - private volatile boolean started = false; + // Shutdown the event consumer threads + if (secondaryEventHandler != null) { + synchronized (secondaryEventHandler) { + secondaryEventHandler.notify(); + } + } + if (primaryEventHandler != null) { + synchronized (primaryEventHandler) { + primaryEventHandler.notify(); + } + } - /** - * When true, exit the application. - */ - private volatile boolean quit = false; + // Shutdown the user I/O thread(s) + backend.shutdown(); - /** - * When true, repaint the entire screen. - */ - private volatile boolean repaint = true; + // Close all the windows. This gives them an opportunity to release + // resources. + closeAllWindows(); - /** - * Repaint the screen on the next update. - */ - public void doRepaint() { - repaint = true; - wakeEventHandler(); } - /** - * Y coordinate of the top edge of the desktop. For now this is a - * constant. Someday it would be nice to have a multi-line menu or - * toolbars. - */ - private static final int desktopTop = 1; + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ /** - * Get Y coordinate of the top edge of the desktop. + * Method that TApplication subclasses can override to handle menu or + * posted command events. * - * @return Y coordinate of the top edge of the desktop + * @param command command event + * @return if true, this event was consumed */ - public final int getDesktopTop() { - return desktopTop; - } - - /** - * Y coordinate of the bottom edge of the desktop. - */ - private int desktopBottom; + protected boolean onCommand(final TCommandEvent command) { + // Default: handle cmExit + if (command.equals(cmExit)) { + if (messageBox(i18n.getString("exitDialogTitle"), + i18n.getString("exitDialogText"), + TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { + exit(); + } + return true; + } - /** - * 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; - } + if (command.equals(cmShell)) { + openTerminal(0, 0, TWindow.RESIZABLE); + return true; + } - /** - * An optional TDesktop background window that is drawn underneath - * everything else. - */ - private TDesktop desktop; + if (command.equals(cmTile)) { + tileWindows(); + return true; + } + if (command.equals(cmCascade)) { + cascadeWindows(); + return true; + } + if (command.equals(cmCloseAll)) { + closeAllWindows(); + return true; + } - /** - * 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.onClose(); + if (command.equals(cmMenu)) { + if (!modalWindowActive() && (activeMenu == null)) { + if (menus.size() > 0) { + menus.get(0).setActive(true); + activeMenu = menus.get(0); + return true; + } + } } - this.desktop = desktop; - } - /** - * Get the TDesktop instance. - * - * @return the desktop, or null if it is not set - */ - public final TDesktop getDesktop() { - return desktop; + return false; } /** - * Get the current active window. + * Method that TApplication subclasses can override to handle menu + * events. * - * @return the active window, or null if it is not set + * @param menu menu event + * @return if true, this event was consumed */ - public final TWindow getActiveWindow() { - return activeWindow; - } + protected boolean onMenu(final TMenuEvent menu) { - /** - * 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 LinkedList(); - result.addAll(windows); - return result; - } + // Default: handle MID_EXIT + if (menu.getId() == TMenu.MID_EXIT) { + if (messageBox(i18n.getString("exitDialogTitle"), + i18n.getString("exitDialogText"), + TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { + exit(); + } + return true; + } - /** - * If true, focus follows mouse: windows automatically raised if the - * mouse passes over them. - */ - private boolean focusFollowsMouse = false; + if (menu.getId() == TMenu.MID_SHELL) { + openTerminal(0, 0, TWindow.RESIZABLE); + return true; + } - /** - * Get focusFollowsMouse flag. - * - * @return true if focus follows mouse: windows automatically raised if - * the mouse passes over them - */ - public boolean getFocusFollowsMouse() { - return focusFollowsMouse; + 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) { + doRepaint(); + return true; + } + return false; } /** - * Set focusFollowsMouse flag. + * Method that TApplication subclasses can override to handle keystrokes. * - * @param focusFollowsMouse if true, focus follows mouse: windows - * automatically raised if the mouse passes over them + * @param keypress keystroke event + * @return if true, this event was consumed */ - public void setFocusFollowsMouse(final boolean focusFollowsMouse) { - this.focusFollowsMouse = focusFollowsMouse; - } + protected boolean onKeypress(final TKeypressEvent keypress) { + // Default: only menu shortcuts - // ------------------------------------------------------------------------ - // General behavior ------------------------------------------------------- - // ------------------------------------------------------------------------ + // Process Alt-F, Alt-E, etc. menu shortcut keys + if (!keypress.getKey().isFnKey() + && keypress.getKey().isAlt() + && !keypress.getKey().isCtrl() + && (activeMenu == null) + && !modalWindowActive() + ) { - /** - * Display the about dialog. - */ - protected void showAboutDialog() { - messageBox(i18n.getString("aboutDialogTitle"), - MessageFormat.format(i18n.getString("aboutDialogText"), - this.getClass().getPackage().getImplementationVersion()), - TMessageBox.Type.OK); - } + assert (subMenus.size() == 0); - // ------------------------------------------------------------------------ - // Constructors ----------------------------------------------------------- - // ------------------------------------------------------------------------ + for (TMenu menu: menus) { + if (Character.toLowerCase(menu.getMnemonic().getShortcut()) + == Character.toLowerCase(keypress.getKey().getChar()) + ) { + activeMenu = menu; + menu.setActive(true); + return true; + } + } + } + + return false; + } /** - * 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 + * Process background events, and update the screen. */ - public TApplication(final BackendType backendType, final int windowWidth, - final int windowHeight, final int fontSize) - throws UnsupportedEncodingException { + private void finishEventProcessing() { + if (debugThreads) { + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " finishEventProcessing()\n"); + } - 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); + // Process timers and call doIdle()'s + doIdle(); + + // Update the screen + synchronized (getScreen()) { + drawAll(); + } + + if (debugThreads) { + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " finishEventProcessing() END\n"); } - TApplicationImpl(); } /** - * Public constructor. + * Peek at certain application-level events, add to eventQueue, and wake + * up the consuming Thread. * - * @param backendType BackendType.XTERM, BackendType.ECMA48 or - * BackendType.SWING - * @throws UnsupportedEncodingException if an exception is thrown when - * creating the InputStreamReader + * @param event the input event to consume */ - public TApplication(final BackendType backendType) - throws UnsupportedEncodingException { + private void metaHandleEvent(final TInputEvent event) { - 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); + if (debugEvents) { + System.err.printf(String.format("metaHandleEvents event: %s\n", + event)); System.err.flush(); } - 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(); - } - - /** - * 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. - * @param setRawMode if true, set System.in into raw mode with stty. - * This should in general not be used. It is here solely for Demo3, - * which uses System.in. - * @throws IllegalArgumentException if input, reader, or writer are null. - */ - public TApplication(final InputStream input, final Reader reader, - final PrintWriter writer, final boolean setRawMode) { - - backend = new ECMA48Backend(this, input, reader, writer, setRawMode); - TApplicationImpl(); - } - - /** - * 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(); - } + if (quit) { + // Do no more processing if the application is already trying + // to exit. + return; + } - /** - * Finish construction once the backend is set. - */ - 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(); - desktop = new TDesktop(this); + // Special application-wide events ------------------------------- - // 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(); - } - } - ); + // Abort everything + if (event instanceof TCommandEvent) { + TCommandEvent command = (TCommandEvent) event; + if (command.getCmd().equals(cmAbort)) { + exit(); + return; } } - } - // ------------------------------------------------------------------------ - // Screen refresh loop ---------------------------------------------------- - // ------------------------------------------------------------------------ + synchronized (drainEventQueue) { + // Screen resize + if (event instanceof TResizeEvent) { + TResizeEvent resize = (TResizeEvent) event; + synchronized (getScreen()) { + getScreen().setDimensions(resize.getWidth(), + resize.getHeight()); + desktopBottom = getScreen().getHeight() - 1; + mouseX = 0; + mouseY = 0; + oldMouseX = 0; + oldMouseY = 0; + } + if (desktop != null) { + desktop.setDimensions(0, 0, resize.getWidth(), + resize.getHeight() - 1); + } - /** - * Process background events, and update the screen. - */ - private void finishEventProcessing() { - if (debugThreads) { - System.err.printf(System.currentTimeMillis() + " " + - Thread.currentThread() + " finishEventProcessing()\n"); - } + // Change menu edges if needed. + recomputeMenuX(); - // Process timers and call doIdle()'s - doIdle(); + // We are dirty, redraw the screen. + doRepaint(); - // Update the screen - synchronized (getScreen()) { - drawAll(); - } + /* + System.err.println("New screen: " + resize.getWidth() + + " x " + resize.getHeight()); + */ + return; + } - if (debugThreads) { - System.err.printf(System.currentTimeMillis() + " " + - Thread.currentThread() + " finishEventProcessing() END\n"); + // Put into the main queue + drainEventQueue.add(event); } } /** - * Invert the cell color at a position. This is used to track the mouse. + * 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 x column position - * @param y row position + * @param event the input event to consume + * @see #secondaryHandleEvent(TInputEvent event) */ - private void invertCell(final int x, final int y) { - if (debugThreads) { - System.err.printf("%d %s invertCell() %d %d\n", - System.currentTimeMillis(), Thread.currentThread(), x, y); + private void primaryHandleEvent(final TInputEvent event) { + + if (debugEvents) { + System.err.printf("Handle event: %s\n", event); } - CellAttributes attr = getScreen().getAttrXY(x, y); - attr.setForeColor(attr.getForeColor().invert()); - attr.setBackColor(attr.getBackColor().invert()); - getScreen().putAttrXY(x, y, attr, false); - } + TMouseEvent doubleClick = null; - /** - * Draw everything. - */ - private void drawAll() { - boolean menuIsActive = false; + // Special application-wide events ----------------------------------- - if (debugThreads) { - System.err.printf("%d %s drawAll() enter\n", - System.currentTimeMillis(), Thread.currentThread()); - } + // Peek at the mouse position + if (event instanceof TMouseEvent) { + 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_UP) { + if ((mouse.getTime().getTime() - lastMouseUpTime) < + doubleClickTime) { - if (!repaint) { - if (debugThreads) { - System.err.printf("%d %s drawAll() !repaint\n", - System.currentTimeMillis(), Thread.currentThread()); - } - 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(); + // 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(); + } } - return; } - } - if (debugThreads) { - System.err.printf("%d %s drawAll() REDRAW\n", - System.currentTimeMillis(), Thread.currentThread()); + // See if we need to switch focus to another window or the menu + checkSwitchFocus((TMouseEvent) event); } - // If true, the cursor is not visible - boolean cursor = false; - - // Start with a clean screen - getScreen().clear(); + // Handle menu events + if ((activeMenu != null) && !(event instanceof TCommandEvent)) { + TMenu menu = activeMenu; - // Draw the desktop - if (desktop != null) { - desktop.drawChildren(); - } + if (event instanceof TMouseEvent) { + TMouseEvent mouse = (TMouseEvent) event; - // Draw each window in reverse Z order - List sorted = new LinkedList(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(); - } - } - - // 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, 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); + 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(); + } - if (menu.isActive()) { - menu.drawChildren(); - // Reset the screen clipping so we can draw the next title. - getScreen().resetClipping(); + // 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()); } - x += menu.getTitle().length() + 2; - } - - for (TMenu menu: subMenus) { - // Reset the screen clipping so we can draw the next sub-menu. - getScreen().resetClipping(); - menu.drawChildren(); + menu.handleEvent(event); + return; } - // 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); - } + if (event instanceof TKeypressEvent) { + TKeypressEvent keypress = (TKeypressEvent) event; - // Draw the mouse pointer - invertCell(mouseX, mouseY); - oldMouseX = mouseX; - oldMouseY = mouseY; + // 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; + } + } - // Place the cursor if it is visible - if (!menuIsActive) { - TWidget activeWidget = null; - if (sorted.size() > 0) { - activeWidget = sorted.get(sorted.size() - 1).getActiveChild(); - if (activeWidget.isCursorVisible()) { - if ((activeWidget.getCursorAbsoluteY() < desktopBottom) - && (activeWidget.getCursorAbsoluteY() > desktopTop) - ) { - getScreen().putCursor(true, - activeWidget.getCursorAbsoluteX(), - activeWidget.getCursorAbsoluteY()); - cursor = true; - } else { - getScreen().putCursor(false, - activeWidget.getCursorAbsoluteX(), - activeWidget.getCursorAbsoluteY()); - cursor = false; + 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; + } } } - // Kill the cursor - if (!cursor) { - getScreen().hideCursor(); + if (event instanceof TCommandEvent) { + if (onCommand((TCommandEvent) event)) { + return; + } } - // Flush the screen contents - if (getScreen().isDirty()) { - backend.flushScreen(); + if (event instanceof TMenuEvent) { + if (onMenu((TMenuEvent) event)) { + return; + } } - repaint = false; - } + // 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()); - // ------------------------------------------------------------------------ - // Main loop -------------------------------------------------------------- - // ------------------------------------------------------------------------ + if (doubleClick != null) { + doubleClick.setX(doubleClick.getX() - window.getX()); + doubleClick.setY(doubleClick.getY() - window.getY()); + } - /** - * Force this application to exit. - */ - public void exit() { - quit = true; - synchronized (this) { - this.notify(); + if (window.mouseWouldHit(mouse)) { + dispatchToDesktop = false; + } + } else if (event instanceof TKeypressEvent) { + 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); + } + } } } /** - * Run this application until it exits. + * 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) */ - public void run() { - // Start the main consumer thread - primaryEventHandler = new WidgetEventHandler(this, true); - (new Thread(primaryEventHandler)).start(); + private void secondaryHandleEvent(final TInputEvent event) { + TMouseEvent doubleClick = null; - started = true; + // Peek at the mouse position + if (event instanceof TMouseEvent) { + 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_UP) { + if ((mouse.getTime().getTime() - lastMouseUpTime) < + doubleClickTime) { - while (!quit) { - synchronized (this) { - boolean doWait = false; + // 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()); - synchronized (fillEventQueue) { - if (fillEventQueue.size() == 0) { - doWait = true; + } else { + // The first click of a potential double-click. + lastMouseUpTime = mouse.getTime().getTime(); } } + } + } - if (doWait) { - // No I/O to dispatch, so wait until the backend - // provides new I/O. - try { - if (debugThreads) { - System.err.println(System.currentTimeMillis() + - " MAIN sleep"); - } + secondaryEventReceiver.handleEvent(event); + if (doubleClick != null) { + secondaryEventReceiver.handleEvent(doubleClick); + } + } - this.wait(); + /** + * 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()"); + } - if (debugThreads) { - System.err.println(System.currentTimeMillis() + - " MAIN AWAKE"); - } - } catch (InterruptedException e) { - // I'm awake and don't care why, let's see what's - // going on out there. - } + 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() { + assert (secondaryEventReceiver != null); + + while (secondaryEventReceiver != null) { + synchronized (primaryEventHandler) { + try { + primaryEventHandler.wait(); + } catch (InterruptedException e) { + // SQUASH } + } + } + } - } // synchronized (this) + /** + * Do stuff when there is no user input. + */ + private void doIdle() { + if (debugThreads) { + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " doIdle()\n"); + } - synchronized (fillEventQueue) { - // Pull any pending I/O events - backend.getEvents(fillEventQueue); + synchronized (timers) { - // Dispatch each event to the appropriate handler, one at a - // time. - for (;;) { - TInputEvent event = null; - if (fillEventQueue.size() == 0) { - break; + 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); } - event = fillEventQueue.remove(0); - metaHandleEvent(event); + } else { + keepTimers.add(timer); } } + timers = keepTimers; + } - // Wake a consumer thread if we have any pending events. - if (drainEventQueue.size() > 0) { - wakeEventHandler(); - } + // Call onIdle's + for (TWindow window: windows) { + window.onIdle(); + } + if (desktop != null) { + desktop.onIdle(); + } + } - } // while (!quit) + /** + * Wake the sleeping active event handler. + */ + private void wakeEventHandler() { + if (!started) { + return; + } - // Shutdown the event consumer threads if (secondaryEventHandler != null) { synchronized (secondaryEventHandler) { secondaryEventHandler.notify(); } - } - if (primaryEventHandler != null) { + } else { + assert (primaryEventHandler != null); synchronized (primaryEventHandler) { primaryEventHandler.notify(); } } + } - // Shutdown the user I/O thread(s) - backend.shutdown(); - - // Close all the windows. This gives them an opportunity to release - // resources. - closeAllWindows(); + // ------------------------------------------------------------------------ + // TApplication ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** + * Get the Backend. + * + * @return the Backend + */ + public final Backend getBackend() { + return backend; } /** - * Peek at certain application-level events, add to eventQueue, and wake - * up the consuming Thread. + * Get the Screen. * - * @param event the input event to consume + * @return the Screen */ - 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; + 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(); } + } - // Special application-wide events ------------------------------- - - // Abort everything - if (event instanceof TCommandEvent) { - TCommandEvent command = (TCommandEvent) event; - if (command.getCmd().equals(cmAbort)) { - exit(); - return; - } - } + /** + * Get the color theme. + * + * @return the theme + */ + public final ColorTheme getTheme() { + return theme; + } - synchronized (drainEventQueue) { - // Screen resize - if (event instanceof TResizeEvent) { - TResizeEvent resize = (TResizeEvent) event; - synchronized (getScreen()) { - getScreen().setDimensions(resize.getWidth(), - resize.getHeight()); - desktopBottom = getScreen().getHeight() - 1; - mouseX = 0; - mouseY = 0; - oldMouseX = 0; - oldMouseY = 0; - } - if (desktop != null) { - desktop.setDimensions(0, 0, resize.getWidth(), - resize.getHeight() - 1); - } + /** + * Repaint the screen on the next update. + */ + public void doRepaint() { + repaint = true; + wakeEventHandler(); + } - // Change menu edges if needed. - recomputeMenuX(); + /** + * 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; + } - // We are dirty, redraw the screen. - doRepaint(); - return; - } + /** + * 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; + } - // Put into the main queue - drainEventQueue.add(event); + /** + * 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.onClose(); } + this.desktop = desktop; } /** - * 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. + * Get the TDesktop instance. * - * @param event the input event to consume - * @see #secondaryHandleEvent(TInputEvent event) + * @return the desktop, or null if it is not set */ - private void primaryHandleEvent(final TInputEvent event) { - - if (debugEvents) { - System.err.printf("Handle event: %s\n", event); - } - TMouseEvent doubleClick = null; + public final TDesktop getDesktop() { + return desktop; + } - // Special application-wide events ----------------------------------- + /** + * Get the current active window. + * + * @return the active window, or null if it is not set + */ + public final TWindow getActiveWindow() { + return activeWindow; + } - // Peek at the mouse position - if (event instanceof TMouseEvent) { - 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_UP) { - if ((mouse.getTime().getTime() - lastMouseUpTime) < - doubleClickTime) { + /** + * 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 LinkedList(); + result.addAll(windows); + return result; + } - // 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()); + /** + * Get focusFollowsMouse flag. + * + * @return true if focus follows mouse: windows automatically raised if + * the mouse passes over them + */ + public boolean getFocusFollowsMouse() { + return focusFollowsMouse; + } - } else { - // The first click of a potential double-click. - lastMouseUpTime = mouse.getTime().getTime(); - } - } - } + /** + * 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; + } - // See if we need to switch focus to another window or the menu - checkSwitchFocus((TMouseEvent) event); - } + /** + * Display the about dialog. + */ + protected void showAboutDialog() { + messageBox(i18n.getString("aboutDialogTitle"), + MessageFormat.format(i18n.getString("aboutDialogText"), + this.getClass().getPackage().getImplementationVersion()), + TMessageBox.Type.OK); + } - // Handle menu events - if ((activeMenu != null) && !(event instanceof TCommandEvent)) { - TMenu menu = activeMenu; + // ------------------------------------------------------------------------ + // Screen refresh loop ---------------------------------------------------- + // ------------------------------------------------------------------------ - if (event instanceof TMouseEvent) { - TMouseEvent mouse = (TMouseEvent) event; + /** + * 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) { + if (debugThreads) { + System.err.printf("%d %s invertCell() %d %d\n", + System.currentTimeMillis(), Thread.currentThread(), x, y); + } + CellAttributes attr = getScreen().getAttrXY(x, y); + attr.setForeColor(attr.getForeColor().invert()); + attr.setBackColor(attr.getBackColor().invert()); + getScreen().putAttrXY(x, y, attr, false); + } - 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(); - } + /** + * Draw everything. + */ + private void drawAll() { + boolean menuIsActive = false; - // 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 (debugThreads) { + System.err.printf("%d %s drawAll() enter\n", + System.currentTimeMillis(), Thread.currentThread()); } - 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 (!repaint) { + if (debugThreads) { + System.err.printf("%d %s drawAll() !repaint\n", + System.currentTimeMillis(), Thread.currentThread()); } - - 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; - } + 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; } - - // Handle the keypress - if (onKeypress(keypress)) { - return; + if (getScreen().isDirty()) { + backend.flushScreen(); } - } - } - - if (event instanceof TCommandEvent) { - if (onCommand((TCommandEvent) event)) { return; } } - if (event instanceof TMenuEvent) { - if (onMenu((TMenuEvent) event)) { - return; - } + if (debugThreads) { + System.err.printf("%d %s drawAll() REDRAW\n", + System.currentTimeMillis(), Thread.currentThread()); } - // 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 true, the cursor is not visible + boolean cursor = false; - if (doubleClick != null) { - doubleClick.setX(doubleClick.getX() - window.getX()); - doubleClick.setY(doubleClick.getY() - window.getY()); - } + // Start with a clean screen + getScreen().clear(); - if (window.mouseWouldHit(mouse)) { - dispatchToDesktop = false; - } - } else if (event instanceof TKeypressEvent) { - dispatchToDesktop = false; - } + // Draw the desktop + if (desktop != null) { + desktop.drawChildren(); + } - if (debugEvents) { - System.err.printf("TApplication dispatch event: %s\n", - event); - } - window.handleEvent(event); - if (doubleClick != null) { - window.handleEvent(doubleClick); + // Draw each window in reverse Z order + List sorted = new LinkedList(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 (dispatchToDesktop) { - // This event is fair game for the desktop to process. - if (desktop != null) { - desktop.handleEvent(event); - if (doubleClick != null) { - desktop.handleEvent(doubleClick); - } + + // 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, 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(); } + x += menu.getTitle().length() + 2; } - } - /** - * 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; + for (TMenu menu: subMenus) { + // Reset the screen clipping so we can draw the next sub-menu. + getScreen().resetClipping(); + menu.drawChildren(); + } - // Peek at the mouse position - if (event instanceof TMouseEvent) { - 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_UP) { - if ((mouse.getTime().getTime() - lastMouseUpTime) < - doubleClickTime) { + // 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); + } - // 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()); + // Draw the mouse pointer + invertCell(mouseX, mouseY); + oldMouseX = mouseX; + oldMouseY = mouseY; + // Place the cursor if it is visible + if (!menuIsActive) { + TWidget activeWidget = null; + if (sorted.size() > 0) { + activeWidget = sorted.get(sorted.size() - 1).getActiveChild(); + if (activeWidget.isCursorVisible()) { + if ((activeWidget.getCursorAbsoluteY() < desktopBottom) + && (activeWidget.getCursorAbsoluteY() > desktopTop) + ) { + getScreen().putCursor(true, + activeWidget.getCursorAbsoluteX(), + activeWidget.getCursorAbsoluteY()); + cursor = true; } else { - // The first click of a potential double-click. - lastMouseUpTime = mouse.getTime().getTime(); + getScreen().putCursor(false, + activeWidget.getCursorAbsoluteX(), + activeWidget.getCursorAbsoluteY()); + cursor = false; } } } } - secondaryEventReceiver.handleEvent(event); - if (doubleClick != null) { - secondaryEventReceiver.handleEvent(doubleClick); + // Kill the cursor + if (!cursor) { + getScreen().hideCursor(); } - } - /** - * 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()"); + // Flush the screen contents + if (getScreen().isDirty()) { + backend.flushScreen(); } - 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() { - assert (secondaryEventReceiver != null); - - while (secondaryEventReceiver != null) { - synchronized (primaryEventHandler) { - try { - primaryEventHandler.wait(); - } catch (InterruptedException e) { - // SQUASH - } - } - } + repaint = false; } /** - * Do stuff when there is no user input. + * Force this application to exit. */ - 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 = keepTimers; - } - - // Call onIdle's - for (TWindow window: windows) { - window.onIdle(); - } - if (desktop != null) { - desktop.onIdle(); + public void exit() { + quit = true; + synchronized (this) { + this.notify(); } } @@ -1770,7 +1906,7 @@ public class TApplication implements Runnable { * * @param window new window to add */ - public final void addWindow(final TWindow window) { + public final void addWindowToApplication(final TWindow window) { // Do not add menu windows to the window list. if (window instanceof TMenu) { @@ -1816,7 +1952,9 @@ public class TApplication implements Runnable { } if (((window.flags & TWindow.CENTERED) == 0) - && smartWindowPlacement) { + && ((window.flags & TWindow.ABSOLUTEXY) == 0) + && (smartWindowPlacement == true) + ) { doSmartPlacement(window); } @@ -2577,139 +2715,6 @@ public class TApplication implements Runnable { return helpMenu; } - // ------------------------------------------------------------------------ - // 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).getResult() == TMessageBox.Result.YES) { - 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)) { - 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).getResult() == TMessageBox.Result.YES) { - 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) { - doRepaint(); - 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() - ) { - - 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; - } - // ------------------------------------------------------------------------ // TTimer management ------------------------------------------------------ // ------------------------------------------------------------------------