X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTApplication.java;h=20406b1f8e82aa654efa86081c085d5f3883af03;hb=00691e80f2f135f92be739e2b7e86775a2357276;hp=105f1ce97cfa5a047e9502a1c8bf9877d6200fa8;hpb=be72cb5ccbd42fe304c0acafc380c5636f0d03a2;p=nikiroo-utils.git diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 105f1ce..20406b1 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 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,20 +28,24 @@ */ 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.event.TCommandEvent; @@ -51,8 +55,8 @@ import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; import jexer.backend.Backend; -import jexer.backend.Screen; import jexer.backend.MultiBackend; +import jexer.backend.Screen; import jexer.backend.SwingBackend; import jexer.backend.ECMA48Backend; import jexer.backend.TWindowBackend; @@ -68,8 +72,13 @@ import static jexer.TKeypress.*; */ public class TApplication implements Runnable { + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(TApplication.class.getName()); + // ------------------------------------------------------------------------ - // Public constants ------------------------------------------------------- + // Constants -------------------------------------------------------------- // ------------------------------------------------------------------------ /** @@ -109,9 +118,180 @@ 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; + + /** + * Old drawn version of mouse coordinate X. + */ + private int oldDrawnMouseX; + + /** + * Old drawn version mouse coordinate Y. + */ + private int oldDrawnMouseY; + + /** + * Old drawn version mouse cell. + */ + private Cell oldDrawnMouseCell = new Cell(); + + /** + * 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; + + /** + * The images that might be displayed. Note package private access. + */ + private List images; + + /** + * The list of commands to run before the next I/O check. + */ + private List invokeLaters = new LinkedList(); + /** * WidgetEventHandler is the main event consumer loop. There are at most * two such threads in existence: the primary for normal case and a @@ -146,6 +326,21 @@ public class TApplication implements Runnable { * 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 @@ -173,9 +368,10 @@ public class TApplication implements Runnable { } if (debugThreads) { - System.err.printf("%d %s %s sleep %d millis\n", + System.err.printf("%d %s %s %s sleep %d millis\n", System.currentTimeMillis(), this, - primary ? "primary" : "secondary", timeout); + primary ? "primary" : "secondary", + Thread.currentThread(), timeout); } synchronized (this) { @@ -183,9 +379,10 @@ public class TApplication implements Runnable { } if (debugThreads) { - System.err.printf("%d %s %s AWAKE\n", + System.err.printf("%d %s %s %s AWAKE\n", System.currentTimeMillis(), this, - primary ? "primary" : "secondary"); + primary ? "primary" : "secondary", + Thread.currentThread()); } if ((!primary) @@ -231,15 +428,16 @@ public class TApplication implements Runnable { ) { // Secondary thread, time to exit. + // Eliminate my reference so that wakeEventHandler() + // resumes working on the primary. + application.secondaryEventHandler = null; + // 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; @@ -256,306 +454,53 @@ public class TApplication implements Runnable { } } - /** - * 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; - - /** - * Wake the sleeping active event handler. - */ - private void wakeEventHandler() { - if (secondaryEventHandler != null) { - synchronized (secondaryEventHandler) { - secondaryEventHandler.notify(); - } - } else { - assert (primaryEventHandler != null); - synchronized (primaryEventHandler) { - primaryEventHandler.notify(); - } - } - } - // ------------------------------------------------------------------------ - // TApplication attributes ------------------------------------------------ + // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ /** - * Access to the physical screen, keyboard, and mouse. - */ - private Backend backend; - - /** - * Get the Backend. + * Public constructor. * - * @return the Backend + * @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 final Backend getBackend() { - return backend; - } + public TApplication(final BackendType backendType, final int windowWidth, + final int windowHeight, final int fontSize) + throws UnsupportedEncodingException { - /** - * 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(); + 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(); } /** - * Actual mouse coordinate X. + * Public constructor. + * + * @param backendType BackendType.XTERM, BackendType.ECMA48 or + * BackendType.SWING + * @throws UnsupportedEncodingException if an exception is thrown when + * creating the InputStreamReader */ - 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; - - /** - * 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; - - /** - * Get the color theme. - * - * @return the theme - */ - public final ColorTheme getTheme() { - return 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, exit the application. - */ - private volatile boolean quit = false; - - /** - * When true, repaint the entire screen. - */ - private volatile boolean repaint = true; - - /** - * Repaint the screen on the next update. - */ - public void doRepaint() { - repaint = true; - wakeEventHandler(); - } - - /** - * Y coordinate of the top edge of the desktop. For now this is a - * constant. Someday it would be nice to have a multi-line menu or - * 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; - } - - /** - * Y coordinate of the bottom edge of the desktop. - */ - private int desktopBottom; - - /** - * 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; - } - - /** - * An optional TDesktop background window that is drawn underneath - * everything else. - */ - private TDesktop desktop; - - /** - * 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; - } - - /** - * 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 LinkedList(); - result.addAll(windows); - return result; - } - - /** - * If true, focus follows mouse: windows automatically raised if the - * mouse passes over them. - */ - private boolean focusFollowsMouse = false; - - /** - * 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; - } - - // ------------------------------------------------------------------------ - // General behavior ------------------------------------------------------- - // ------------------------------------------------------------------------ - - /** - * Display the about dialog. - */ - protected void showAboutDialog() { - messageBox("About", "Jexer Version " + - this.getClass().getPackage().getImplementationVersion(), - TMessageBox.Type.OK); - } - - // ------------------------------------------------------------------------ - // Constructors ----------------------------------------------------------- - // ------------------------------------------------------------------------ - - /** - * 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 { + public TApplication(final BackendType backendType) + throws UnsupportedEncodingException { switch (backendType) { case SWING: @@ -651,15 +596,16 @@ public class TApplication implements Runnable { private void TApplicationImpl() { theme = new ColorTheme(); desktopBottom = getScreen().getHeight() - 1; - fillEventQueue = new ArrayList(); - drainEventQueue = new ArrayList(); + fillEventQueue = new LinkedList(); + drainEventQueue = new LinkedList(); windows = new LinkedList(); - menus = new LinkedList(); - subMenus = new LinkedList(); + menus = new ArrayList(); + subMenus = new ArrayList(); timers = new LinkedList(); accelerators = new HashMap(); - menuItems = new ArrayList(); + menuItems = new LinkedList(); desktop = new TDesktop(this); + images = new LinkedList(); // Special case: the Swing backend needs to have a timer to drive its // blink state. @@ -685,236 +631,30 @@ public class TApplication implements Runnable { } // ------------------------------------------------------------------------ - // Screen refresh loop ---------------------------------------------------- + // Runnable --------------------------------------------------------------- // ------------------------------------------------------------------------ /** - * Process background events, and update the screen. + * Run this application until it exits. */ - private void finishEventProcessing() { - if (debugThreads) { - System.err.printf(System.currentTimeMillis() + " " + - Thread.currentThread() + " finishEventProcessing()\n"); - } + public void run() { + // Start the main consumer thread + primaryEventHandler = new WidgetEventHandler(this, true); + (new Thread(primaryEventHandler)).start(); - // Process timers and call doIdle()'s - doIdle(); + started = true; - // Update the screen - synchronized (getScreen()) { - drawAll(); - } + while (!quit) { + synchronized (this) { + boolean doWait = false; - if (debugThreads) { - System.err.printf(System.currentTimeMillis() + " " + - Thread.currentThread() + " finishEventProcessing() END\n"); - } - } - - /** - * Invert the cell color at a position. This is used to track the mouse. - * - * @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); - } - - /** - * Draw everything. - */ - private void drawAll() { - if (debugThreads) { - System.err.printf("%d %s drawAll() enter\n", - System.currentTimeMillis(), Thread.currentThread()); - } - - 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(); - } - 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 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()) { - 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; - } - - for (TMenu menu: subMenus) { - // Reset the screen clipping so we can draw the next sub-menu. - getScreen().resetClipping(); - menu.drawChildren(); - } - - // 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 - invertCell(mouseX, mouseY); - oldMouseX = mouseX; - oldMouseY = mouseY; - - // Place the cursor if it is visible - 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; - } - } - } - - // Kill the cursor - if (!cursor) { - getScreen().hideCursor(); - } - - // Flush the screen contents - if (getScreen().isDirty()) { - backend.flushScreen(); - } - - repaint = false; - } - - // ------------------------------------------------------------------------ - // Main loop -------------------------------------------------------------- - // ------------------------------------------------------------------------ - - /** - * Force this application to exit. - */ - public void exit() { - quit = true; - synchronized (this) { - this.notify(); - } - } - - /** - * Run this application until it exits. - */ - public void run() { - // Start the main consumer thread - primaryEventHandler = new WidgetEventHandler(this, true); - (new Thread(primaryEventHandler)).start(); - - while (!quit) { - synchronized (this) { - boolean doWait = false; - - synchronized (fillEventQueue) { - if (fillEventQueue.size() == 0) { - doWait = true; - } - } + if (!backend.hasEvents()) { + synchronized (fillEventQueue) { + if (fillEventQueue.size() == 0) { + doWait = true; + } + } + } if (doWait) { // No I/O to dispatch, so wait until the backend @@ -922,14 +662,14 @@ public class TApplication implements Runnable { try { if (debugThreads) { System.err.println(System.currentTimeMillis() + - " MAIN sleep"); + " " + Thread.currentThread() + " MAIN sleep"); } this.wait(); if (debugThreads) { System.err.println(System.currentTimeMillis() + - " MAIN AWAKE"); + " " + Thread.currentThread() + " MAIN AWAKE"); } } catch (InterruptedException e) { // I'm awake and don't care why, let's see what's @@ -983,74 +723,254 @@ public class TApplication implements Runnable { } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + /** - * Peek at certain application-level events, add to eventQueue, and wake - * up the consuming Thread. + * Method that TApplication subclasses can override to handle menu or + * posted command events. * - * @param event the input event to consume + * @param command command event + * @return if true, this event was consumed */ - private void metaHandleEvent(final TInputEvent event) { + 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()) { - if (debugEvents) { - System.err.printf(String.format("metaHandleEvents event: %s\n", - event)); System.err.flush(); + exit(); + } + return true; } - if (quit) { - // Do no more processing if the application is already trying - // to exit. - return; + if (command.equals(cmShell)) { + openTerminal(0, 0, TWindow.RESIZABLE); + return true; } - // Special application-wide events ------------------------------- - - // Abort everything - if (event instanceof TCommandEvent) { - TCommandEvent command = (TCommandEvent) event; - if (command.getCmd().equals(cmAbort)) { - exit(); - return; - } + if (command.equals(cmTile)) { + tileWindows(); + return true; + } + if (command.equals(cmCascade)) { + cascadeWindows(); + return true; + } + if (command.equals(cmCloseAll)) { + closeAllWindows(); + return true; } - 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); + if (command.equals(cmMenu)) { + if (!modalWindowActive() && (activeMenu == null)) { + if (menus.size() > 0) { + menus.get(0).setActive(true); + activeMenu = menus.get(0); + return true; } - return; } - - // Put into the main queue - drainEventQueue.add(event); } + + return false; } /** - * 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. + * Method that TApplication subclasses can override to handle menu + * events. * - * @param event the input event to consume - * @see #secondaryHandleEvent(TInputEvent event) + * @param menu menu event + * @return if true, this event was consumed */ - private void primaryHandleEvent(final TInputEvent event) { + protected boolean onMenu(final TMenuEvent menu) { - if (debugEvents) { - System.err.printf("Handle event: %s\n", event); - } + // 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_CHANGE_FONT) { + 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() + ) { + + 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(); + } + + 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.getCmd().equals(cmAbort)) { + exit(); + return; + } + } + + 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); + } + + // 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 ----------------------------------- @@ -1062,6 +982,28 @@ public class TApplication implements Runnable { 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 @@ -1169,6 +1111,11 @@ public class TApplication implements Runnable { 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; } @@ -1181,11 +1128,17 @@ public class TApplication implements Runnable { 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); + } } } } @@ -1199,6 +1152,13 @@ public class TApplication implements Runnable { * @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) { TMouseEvent mouse = (TMouseEvent) event; @@ -1207,90 +1167,629 @@ public class TApplication implements Runnable { 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(); + } + } + } + + // ------------------------------------------------------------------------ + // 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.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.0"; + } + 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); + } + } + + // ------------------------------------------------------------------------ + // 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) { + 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; + } + } + + Cell cell = getScreen().getCharXY(x, y); + if (cell.isImage()) { + cell.invertImage(); + } else { + 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); } /** - * Enable a widget to override the primary event thread. - * - * @param widget widget that will receive events + * Draw everything. */ - public final void enableSecondaryEventReceiver(final TWidget widget) { + private void drawAll() { + boolean menuIsActive = false; + if (debugThreads) { - System.err.println(System.currentTimeMillis() + - " enableSecondaryEventReceiver()"); + System.err.printf("%d %s drawAll() enter\n", + System.currentTimeMillis(), Thread.currentThread()); } - assert (secondaryEventReceiver == null); - assert (secondaryEventHandler == null); - assert ((widget instanceof TMessageBox) - || (widget instanceof TFileOpenBox)); - secondaryEventReceiver = widget; - secondaryEventHandler = new WidgetEventHandler(this, false); - - (new Thread(secondaryEventHandler)).start(); - } + // 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()); + } - /** - * Yield to the secondary thread. - */ - public final void yield() { - assert (secondaryEventReceiver != null); + // The only thing that has happened is the mouse moved. - while (secondaryEventReceiver != null) { - synchronized (primaryEventHandler) { - try { - primaryEventHandler.wait(); - } catch (InterruptedException e) { - // SQUASH + // 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 ((images.size() > 0) && (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); + } } + + // Draw mouse at the new position. + invertCell(mouseX, mouseY); + + oldDrawnMouseX = mouseX; + oldDrawnMouseY = mouseY; } + if ((images.size() > 0) || getScreen().isDirty()) { + backend.flushScreen(); + } + return; } - } - /** - * Do stuff when there is no user input. - */ - private void doIdle() { if (debugThreads) { - System.err.printf(System.currentTimeMillis() + " " + - Thread.currentThread() + " doIdle()\n"); + System.err.printf("%d %s drawAll() REDRAW\n", + System.currentTimeMillis(), Thread.currentThread()); } - synchronized (timers) { + // If true, the cursor is not visible + boolean cursor = false; - if (debugThreads) { - System.err.printf(System.currentTimeMillis() + " " + - Thread.currentThread() + " doIdle() 2\n"); + // 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(); } + } - // 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); + // 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()) { + ((TWindow) menu).drawChildren(); + // Reset the screen clipping so we can draw the next title. + getScreen().resetClipping(); + } + x += menu.getTitle().length() + 2; + } + + for (TMenu menu: subMenus) { + // Reset the screen clipping so we can draw the next sub-menu. + getScreen().resetClipping(); + ((TWindow) menu).drawChildren(); + } + getScreen().resetClipping(); + + // 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 ((images.size() > 0) && (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 { - keepTimers.add(timer); } + } else { + getScreen().unsetImageRow(mouseY); } - timers = keepTimers; } + invertCell(mouseX, mouseY); + oldDrawnMouseX = mouseX; + oldDrawnMouseY = mouseY; - // Call onIdle's - for (TWindow window: windows) { - window.onIdle(); + // 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 { + // Turn off the cursor. Also place it at 0,0. + getScreen().putCursor(false, 0, 0); + cursor = false; + } + } + } } - if (desktop != null) { - desktop.onIdle(); + + // Kill the cursor + if (!cursor) { + getScreen().hideCursor(); + } + + // Flush the screen contents + if ((images.size() > 0) || getScreen().isDirty()) { + backend.flushScreen(); + } + + repaint = false; + } + + /** + * Force this application to exit. + */ + public void exit() { + quit = true; + synchronized (this) { + this.notify(); } } @@ -1411,9 +1910,24 @@ public class TApplication implements Runnable { if (activeWindow != null) { assert (activeWindow.getZ() == 0); - activeWindow.onUnfocus(); activeWindow.setActive(false); - activeWindow.setZ(window.getZ()); + + // 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); + } + } + + // 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); @@ -1505,6 +2019,10 @@ public class TApplication implements Runnable { 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) { @@ -1516,23 +2034,33 @@ public class TApplication implements Runnable { int z = window.getZ(); window.setZ(-1); window.onUnfocus(); + windows.remove(window); Collections.sort(windows); - windows.remove(0); activeWindow = null; + int newZ = 0; + boolean foundNextWindow = false; + 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(); - } - } + w.setZ(newZ); + newZ++; + + // Do not activate a hidden window. + if (w.isHidden()) { + continue; + } + + if (foundNextWindow == false) { + foundNextWindow = true; + w.setActive(true); + w.onFocus(); + assert (activeWindow == null); + activeWindow = w; + continue; + } + + if (w.isActive()) { + w.setActive(false); + w.onUnfocus(); } } } @@ -1624,11 +2152,12 @@ public class TApplication implements Runnable { } /** - * Add a window to my window list and make it active. + * Add a window to my window list and make it active. Note package + * private access. * * @param window new window to add */ - public final void addWindow(final TWindow window) { + final void addWindowToApplication(final TWindow window) { // Do not add menu windows to the window list. if (window instanceof TMenu) { @@ -1641,6 +2170,11 @@ public class TApplication implements Runnable { } synchronized (windows) { + if (windows.contains(window)) { + throw new IllegalArgumentException("Window " + window + + " is already in window list"); + } + // Whatever window might be moving/dragging, stop it now. for (TWindow w: windows) { if (w.inMovements()) { @@ -1674,7 +2208,9 @@ public class TApplication implements Runnable { } if (((window.flags & TWindow.CENTERED) == 0) - && smartWindowPlacement) { + && ((window.flags & TWindow.ABSOLUTEXY) == 0) + && (smartWindowPlacement == true) + ) { doSmartPlacement(window); } @@ -1750,7 +2286,7 @@ public class TApplication implements Runnable { int newHeight1 = ((getScreen().getHeight() - 1) / b); int newHeight2 = ((getScreen().getHeight() - 1) / (b + c)); - List sorted = new LinkedList(windows); + List sorted = new ArrayList(windows); Collections.sort(sorted); Collections.reverse(sorted); for (int i = 0; i < sorted.size(); i++) { @@ -1762,6 +2298,9 @@ public class TApplication implements Runnable { } 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)) { @@ -1771,6 +2310,12 @@ public class TApplication implements Runnable { 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())); + } } } } @@ -1786,7 +2331,7 @@ public class TApplication implements Runnable { } int x = 0; int y = 1; - List sorted = new LinkedList(windows); + List sorted = new ArrayList(windows); Collections.sort(sorted); Collections.reverse(sorted); for (TWindow window: sorted) { @@ -1838,10 +2383,16 @@ public class TApplication implements Runnable { 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; } @@ -1929,6 +2480,46 @@ public class TApplication implements Runnable { window.setY(windowY); } + // ------------------------------------------------------------------------ + // TImage management ------------------------------------------------------ + // ------------------------------------------------------------------------ + + /** + * Add an image to the list. Note package private access. + * + * @param image the image to add + * @throws IllegalArgumentException if the image is already used in + * another TApplication + */ + final void addImage(final TImage image) { + if ((image.getApplication() != null) + && (image.getApplication() != this) + ) { + throw new IllegalArgumentException("Image " + image + + " is already " + "part of application " + + image.getApplication()); + } + images.add(image); + } + + /** + * Remove an image from the list. Note package private access. + * + * @param image the image to remove + * @throws IllegalArgumentException if the image is already used in + * another TApplication + */ + final void removeImage(final TImage image) { + if ((image.getApplication() != null) + && (image.getApplication() != this) + ) { + throw new IllegalArgumentException("Image " + image + + " is already " + "part of application " + + image.getApplication()); + } + images.remove(image); + } + // ------------------------------------------------------------------------ // TMenu management ------------------------------------------------------- // ------------------------------------------------------------------------ @@ -1943,7 +2534,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)) { @@ -1990,8 +2581,8 @@ 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() + if ((mouse.getAbsoluteX() >= menu.getTitleX()) + && (mouse.getAbsoluteX() < menu.getTitleX() + menu.getTitle().length() + 2) ) { menu.setActive(true); @@ -2018,8 +2609,8 @@ public class TApplication implements Runnable { // See if we should switch menus for (TMenu menu: menus) { - if ((mouse.getAbsoluteX() >= menu.getX()) - && (mouse.getAbsoluteX() < menu.getX() + if ((mouse.getAbsoluteX() >= menu.getTitleX()) + && (mouse.getAbsoluteX() < menu.getTitleX() + menu.getTitle().length() + 2) ) { menu.setActive(true); @@ -2045,7 +2636,7 @@ public class TApplication implements Runnable { if (((focusFollowsMouse == true) && (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)) - || (mouse.getType() == TMouseEvent.Type.MOUSE_UP) + || (mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) ) { synchronized (windows) { Collections.sort(windows); @@ -2073,9 +2664,11 @@ public class TApplication implements Runnable { assert (windows.get(0).isActive()); assert (windows.get(0) == activeWindow); assert (!window.isActive()); - activeWindow.onUnfocus(); - activeWindow.setActive(false); - activeWindow.setZ(window.getZ()); + if (activeWindow != null) { + activeWindow.onUnfocus(); + activeWindow.setActive(false); + activeWindow.setZ(window.getZ()); + } activeWindow = window; window.setZ(0); window.setActive(true); @@ -2114,7 +2707,7 @@ public class TApplication implements Runnable { * @return a copy of the menu list */ public final List getAllMenus() { - return new LinkedList(menus); + return new ArrayList(menus); } /** @@ -2185,10 +2778,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); @@ -2241,6 +2838,7 @@ public class TApplication implements Runnable { for (TMenuItem item: menuItems) { if ((item.getId() >= lower) && (item.getId() <= upper)) { item.setEnabled(false); + item.getParent().activate(0); } } } @@ -2254,6 +2852,7 @@ public class TApplication implements Runnable { for (TMenuItem item: menuItems) { if (item.getId() == id) { item.setEnabled(true); + item.getParent().activate(0); } } } @@ -2269,6 +2868,7 @@ public class TApplication implements Runnable { for (TMenuItem item: menuItems) { if ((item.getId() >= lower) && (item.getId() <= upper)) { item.setEnabled(true); + item.getParent().activate(0); } } } @@ -2280,7 +2880,32 @@ public class TApplication implements Runnable { int x = 0; for (TMenu menu: menus) { menu.setX(x); + menu.setTitleX(x); x += menu.getTitle().length() + 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(); } } @@ -2327,20 +2952,36 @@ 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_CHANGE_FONT); + 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"); + TMenu fileMenu = addMenu(i18n.getString("fileMenuTitle")); fileMenu.addDefaultItem(TMenu.MID_OPEN_FILE); fileMenu.addSeparator(); fileMenu.addDefaultItem(TMenu.MID_SHELL); fileMenu.addDefaultItem(TMenu.MID_EXIT); - TStatusBar statusBar = fileMenu.newStatusBar("File-management " + - "commands (Open, Save, Print, etc.)"); - statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); + TStatusBar statusBar = fileMenu.newStatusBar(i18n. + getString("fileMenuStatus")); + statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help")); return fileMenu; } @@ -2350,14 +2991,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("Editor operations, " + - "undo, and Clipboard access"); - statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); + TStatusBar statusBar = editMenu.newStatusBar(i18n. + getString("editMenuStatus")); + statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help")); return editMenu; } @@ -2367,7 +3008,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); @@ -2377,9 +3018,9 @@ 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("Open, arrange, and " + - "list windows"); - statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); + TStatusBar statusBar = windowMenu.newStatusBar(i18n. + getString("windowMenuStatus")); + statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help")); return windowMenu; } @@ -2389,7 +3030,7 @@ public class TApplication implements Runnable { * @return the new menu */ public final TMenu addHelpMenu() { - TMenu helpMenu = addMenu("&Help"); + TMenu helpMenu = addMenu(i18n.getString("helpMenuTitle")); helpMenu.addDefaultItem(TMenu.MID_HELP_CONTENTS); helpMenu.addDefaultItem(TMenu.MID_HELP_INDEX); helpMenu.addDefaultItem(TMenu.MID_HELP_SEARCH); @@ -2398,138 +3039,12 @@ public class TApplication implements Runnable { helpMenu.addDefaultItem(TMenu.MID_HELP_ACTIVE_FILE); helpMenu.addSeparator(); helpMenu.addDefaultItem(TMenu.MID_ABOUT); - TStatusBar statusBar = helpMenu.newStatusBar("Access online help"); - statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); + TStatusBar statusBar = helpMenu.newStatusBar(i18n. + getString("helpMenuStatus")); + statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help")); 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("Confirmation", "Exit application?", - 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("Confirmation", "Exit application?", - 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; - } - 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 ------------------------------------------------------ // ------------------------------------------------------------------------ @@ -2655,6 +3170,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. * @@ -2666,6 +3197,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. * @@ -2680,6 +3225,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. * @@ -2708,6 +3366,42 @@ 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). @@ -2715,6 +3409,7 @@ public class TApplication implements Runnable { * @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) { @@ -2722,6 +3417,7 @@ public class TApplication implements Runnable { 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). @@ -2730,6 +3426,7 @@ public class TApplication implements Runnable { * @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) { @@ -2746,6 +3443,7 @@ public class TApplication implements Runnable { * @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) { @@ -2763,6 +3461,7 @@ public class TApplication implements Runnable { * @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,