X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTApplication.java;h=b4c53ec3a5ea8899db40c6dbdda283abe85153f7;hb=499fdccfad144aa58869d839d50edb898670626a;hp=08ad1de436ec3d48504a9e5465869e98335ac7c0;hpb=a2018e9964f6c58742cd1e6dd0a0c63e244a89d6;p=fanfix.git diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 08ad1de..b4c53ec 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -28,12 +28,14 @@ */ 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.Collections; import java.util.Date; import java.util.HashMap; @@ -41,10 +43,10 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.ResourceBundle; import jexer.bits.CellAttributes; import jexer.bits.ColorTheme; -import jexer.bits.GraphicsChars; import jexer.event.TCommandEvent; import jexer.event.TInputEvent; import jexer.event.TKeypressEvent; @@ -52,19 +54,28 @@ 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.SwingBackend; import jexer.backend.ECMA48Backend; -import jexer.io.Screen; +import jexer.backend.TWindowBackend; import jexer.menu.TMenu; import jexer.menu.TMenuItem; import static jexer.TCommand.*; import static jexer.TKeypress.*; /** - * TApplication sets up a full Text User Interface application. + * TApplication is the main driver class for a full Text User Interface + * application. It manages windows, provides a menu bar and status bar, and + * processes events received from the user. */ public class TApplication implements Runnable { + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(TApplication.class.getName()); + // ------------------------------------------------------------------------ // Public constants ------------------------------------------------------- // ------------------------------------------------------------------------ @@ -143,6 +154,7 @@ public class TApplication implements Runnable { * The consumer loop. */ public void run() { + boolean first = true; // Loop forever while (!application.quit) { @@ -156,44 +168,52 @@ public class TApplication implements Runnable { } } - synchronized (this) { - if (debugThreads) { - System.err.printf("%s %s sleep\n", this, - primary ? "primary" : "secondary"); - } + long timeout = 0; + if (first) { + first = false; + } else { + timeout = application.getSleepTime(1000); + } - this.wait(); + if (timeout == 0) { + // A timer needs to fire, break out. + break; + } - if (debugThreads) { - System.err.printf("%s %s AWAKE\n", this, - primary ? "primary" : "secondary"); - } + if (debugThreads) { + System.err.printf("%d %s %s sleep %d millis\n", + System.currentTimeMillis(), this, + primary ? "primary" : "secondary", timeout); + } + + synchronized (this) { + this.wait(timeout); + } + + if (debugThreads) { + System.err.printf("%d %s %s AWAKE\n", + System.currentTimeMillis(), this, + primary ? "primary" : "secondary"); + } - if ((!primary) - && (application.secondaryEventReceiver == null) - ) { - // Secondary thread, emergency exit. If we - // got here then something went wrong with - // the handoff between yield() and - // closeWindow(). - synchronized (application.primaryEventHandler) { - application.primaryEventHandler.notify(); - } - application.secondaryEventHandler = null; - throw new RuntimeException( - "secondary exited at wrong time"); + if ((!primary) + && (application.secondaryEventReceiver == null) + ) { + // Secondary thread, emergency exit. If we got + // here then something went wrong with the + // handoff between yield() and closeWindow(). + synchronized (application.primaryEventHandler) { + application.primaryEventHandler.notify(); } - break; + application.secondaryEventHandler = null; + throw new RuntimeException("secondary exited " + + "at wrong time"); } + break; } catch (InterruptedException e) { // SQUASH } - } - - // Wait for drawAll() or doIdle() to be done, then handle the - // events. - boolean oldLock = lockHandleEvent(); - assert (oldLock == false); + } // while (!application.quit) // Pull all events off the queue for (;;) { @@ -204,7 +224,11 @@ public class TApplication implements Runnable { } event = application.drainEventQueue.remove(0); } + + // We will have an event to process, so repaint the + // screen at the end. application.repaint = true; + if (primary) { primaryHandleEvent(event); } else { @@ -228,17 +252,12 @@ public class TApplication implements Runnable { // All done! return; } - } // for (;;) - // Unlock. Either I am primary thread, or I am secondary - // thread and still running. - oldLock = unlockHandleEvent(); - assert (oldLock == true); + } // for (;;) - // I have done some work of some kind. Tell the main run() - // loop to wake up now. - synchronized (application) { - application.notify(); + // Fire timers, update screen. + if (!quit) { + application.finishEventProcessing(); } } // while (true) (main runnable loop) @@ -260,12 +279,6 @@ public class TApplication implements Runnable { */ private volatile TWidget secondaryEventReceiver; - /** - * Spinlock for the primary and secondary event handlers. - * WidgetEventHandler.run() is responsible for setting this value. - */ - private volatile boolean insideHandleEvent = false; - /** * Wake the sleeping active event handler. */ @@ -282,104 +295,6 @@ public class TApplication implements Runnable { } } - /** - * Set the insideHandleEvent flag to true. lockoutEventHandlers() will - * spin indefinitely until unlockHandleEvent() is called. - * - * @return the old value of insideHandleEvent - */ - private boolean lockHandleEvent() { - if (debugThreads) { - System.err.printf(" >> lockHandleEvent(): oldValue %s", - insideHandleEvent); - } - boolean oldValue = true; - - synchronized (this) { - // Wait for TApplication.run() to finish using the global state - // before allowing further event processing. - while (lockoutHandleEvent == true) { - try { - // Backoff so that the backend can finish its work. - Thread.sleep(5); - } catch (InterruptedException e) { - // SQUASH - } - } - - oldValue = insideHandleEvent; - insideHandleEvent = true; - } - - if (debugThreads) { - System.err.printf(" ***\n"); - } - return oldValue; - } - - /** - * Set the insideHandleEvent flag to false. lockoutEventHandlers() will - * spin indefinitely until unlockHandleEvent() is called. - * - * @return the old value of insideHandleEvent - */ - private boolean unlockHandleEvent() { - if (debugThreads) { - System.err.printf(" << unlockHandleEvent(): oldValue %s\n", - insideHandleEvent); - } - synchronized (this) { - boolean oldValue = insideHandleEvent; - insideHandleEvent = false; - return oldValue; - } - } - - /** - * Spinlock for the primary and secondary event handlers. When true, the - * event handlers will spinlock wait before calling handleEvent(). - */ - private volatile boolean lockoutHandleEvent = false; - - /** - * TApplication.run() needs to be able rely on the global data structures - * being intact when calling doIdle() and drawAll(). Tell the event - * handlers to wait for an unlock before handling their events. - */ - private void stopEventHandlers() { - if (debugThreads) { - System.err.printf(">> stopEventHandlers()"); - } - - lockoutHandleEvent = true; - // Wait for the last event to finish processing before returning - // control to TApplication.run(). - while (insideHandleEvent == true) { - try { - // Backoff so that the event handler can finish its work. - Thread.sleep(1); - } catch (InterruptedException e) { - // SQUASH - } - } - - if (debugThreads) { - System.err.printf(" XXX\n"); - } - } - - /** - * TApplication.run() needs to be able rely on the global data structures - * being intact when calling doIdle() and drawAll(). Tell the event - * handlers that it is now OK to handle their events. - */ - private void startEventHandlers() { - if (debugThreads) { - System.err.printf("<< startEventHandlers()\n"); - } - lockoutHandleEvent = false; - } - // ------------------------------------------------------------------------ // TApplication attributes ------------------------------------------------ // ------------------------------------------------------------------------ @@ -404,7 +319,15 @@ public class TApplication implements Runnable { * @return the Screen */ public final Screen getScreen() { - return backend.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(); + } } /** @@ -449,7 +372,7 @@ public class TApplication implements Runnable { private List subMenus; /** - * The currently acive menu. + * The currently active menu. */ private TMenu activeMenu = null; @@ -482,6 +405,11 @@ public class TApplication implements Runnable { */ private List windows; + /** + * The currently acive window. + */ + private TWindow activeWindow = null; + /** * Timers that are being ticked. */ @@ -497,6 +425,14 @@ public class TApplication implements Runnable { */ 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 @@ -527,6 +463,80 @@ public class TApplication implements Runnable { 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 ------------------------------------------------------- // ------------------------------------------------------------------------ @@ -535,8 +545,9 @@ public class TApplication implements Runnable { * Display the about dialog. */ protected void showAboutDialog() { - messageBox("About", "Jexer Version " + - this.getClass().getPackage().getImplementationVersion(), + messageBox(i18n.getString("aboutDialogTitle"), + MessageFormat.format(i18n.getString("aboutDialogText"), + this.getClass().getPackage().getImplementationVersion()), TMessageBox.Type.OK); } @@ -557,6 +568,12 @@ public class TApplication implements Runnable { 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: @@ -633,6 +650,7 @@ public class TApplication implements Runnable { */ public TApplication(final Backend backend) { this.backend = backend; + backend.setListener(this); TApplicationImpl(); } @@ -650,16 +668,58 @@ public class TApplication implements Runnable { timers = new LinkedList(); accelerators = new HashMap(); menuItems = new ArrayList(); + desktop = new TDesktop(this); - // Setup the main consumer thread - primaryEventHandler = new WidgetEventHandler(this, true); - (new Thread(primaryEventHandler)).start(); + // 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(); + } + } + ); + } + } } // ------------------------------------------------------------------------ // Screen refresh loop ---------------------------------------------------- // ------------------------------------------------------------------------ + /** + * 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"); + } + } + /** * Invert the cell color at a position. This is used to track the mouse. * @@ -668,7 +728,8 @@ public class TApplication implements Runnable { */ private void invertCell(final int x, final int y) { if (debugThreads) { - System.err.printf("invertCell() %d %d\n", x, y); + 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()); @@ -680,13 +741,17 @@ public class TApplication implements Runnable { * Draw everything. */ private void drawAll() { + boolean menuIsActive = false; + if (debugThreads) { - System.err.printf("drawAll() enter\n"); + System.err.printf("%d %s drawAll() enter\n", + System.currentTimeMillis(), Thread.currentThread()); } if (!repaint) { if (debugThreads) { - System.err.printf("drawAll() !repaint\n"); + System.err.printf("%d %s drawAll() !repaint\n", + System.currentTimeMillis(), Thread.currentThread()); } synchronized (getScreen()) { if ((oldMouseX != mouseX) || (oldMouseY != mouseY)) { @@ -705,7 +770,8 @@ public class TApplication implements Runnable { } if (debugThreads) { - System.err.printf("drawAll() REDRAW\n"); + System.err.printf("%d %s drawAll() REDRAW\n", + System.currentTimeMillis(), Thread.currentThread()); } // If true, the cursor is not visible @@ -714,17 +780,23 @@ public class TApplication implements Runnable { // Start with a clean screen getScreen().clear(); - // Draw the background - CellAttributes background = theme.getColor("tapplication.background"); - getScreen().putAll(GraphicsChars.HATCH, background); + // 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 = sorted.get(0); + TWindow topLevel = null; + if (sorted.size() > 0) { + topLevel = sorted.get(0); + } Collections.reverse(sorted); for (TWindow window: sorted) { - window.drawChildren(); + if (window.isShown()) { + window.drawChildren(); + } } // Draw the blank menubar line - reset the screen clipping first so @@ -738,6 +810,7 @@ public class TApplication implements Runnable { CellAttributes menuColor; CellAttributes menuMnemonicColor; if (menu.isActive()) { + menuIsActive = true; menuColor = theme.getColor("tmenu.highlighted"); menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted"); topLevel = menu; @@ -768,7 +841,10 @@ public class TApplication implements Runnable { } // Draw the status bar of the top-level window - TStatusBar statusBar = topLevel.getStatusBar(); + TStatusBar statusBar = null; + if (topLevel != null) { + statusBar = topLevel.getStatusBar(); + } if (statusBar != null) { getScreen().resetClipping(); statusBar.setWidth(getScreen().getWidth()); @@ -787,13 +863,25 @@ public class TApplication implements Runnable { 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()) { - getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(), - activeWidget.getCursorAbsoluteY()); - cursor = true; + 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; + } + } } } @@ -814,60 +902,71 @@ public class TApplication implements Runnable { // 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) { - // Timeout is in milliseconds, so default timeout after 1 second - // of inactivity. - long timeout = 0; - - // If I've got no updates to render, wait for something from the - // backend or a timer. - if (!repaint - && ((mouseX == oldMouseX) && (mouseY == oldMouseY)) - ) { - // Never sleep longer than 50 millis. We need time for - // windows with background tasks to update the display, and - // still flip buffers reasonably quickly in - // backend.flushPhysical(). - timeout = getSleepTime(50); - } - - if (timeout > 0) { - // As of now, I've got nothing to do: no I/O, nothing from - // the consumer threads, no timers that need to run ASAP. So - // wait until either the backend or the consumer threads have - // something to do. - try { - if (debugThreads) { - System.err.println("sleep " + timeout + " millis"); + synchronized (this) { + boolean doWait = false; + + synchronized (fillEventQueue) { + if (fillEventQueue.size() == 0) { + doWait = true; } - synchronized (this) { - this.wait(timeout); + } + + 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"); + } + + this.wait(); + + 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. } - } catch (InterruptedException e) { - // I'm awake and don't care why, let's see what's going - // on out there. } - repaint = true; - } - // Prevent stepping on the primary or secondary event handler. - stopEventHandlers(); + } // synchronized (this) - // Pull any pending I/O events - backend.getEvents(fillEventQueue); + synchronized (fillEventQueue) { + // Pull any pending I/O events + backend.getEvents(fillEventQueue); - // Dispatch each event to the appropriate handler, one at a time. - for (;;) { - TInputEvent event = null; - if (fillEventQueue.size() == 0) { - break; + // 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); } - event = fillEventQueue.remove(0); - metaHandleEvent(event); } // Wake a consumer thread if we have any pending events. @@ -875,17 +974,6 @@ public class TApplication implements Runnable { wakeEventHandler(); } - // Process timers and call doIdle()'s - doIdle(); - - // Update the screen - synchronized (getScreen()) { - drawAll(); - } - - // Let the event handlers run again. - startEventHandlers(); - } // while (!quit) // Shutdown the event consumer threads @@ -934,41 +1022,40 @@ public class TApplication implements Runnable { if (event instanceof TCommandEvent) { TCommandEvent command = (TCommandEvent) event; if (command.getCmd().equals(cmAbort)) { - quit = true; + exit(); return; } } - // Screen resize - if (event instanceof TResizeEvent) { - TResizeEvent resize = (TResizeEvent) event; - synchronized (getScreen()) { - getScreen().setDimensions(resize.getWidth(), - resize.getHeight()); - desktopBottom = getScreen().getHeight() - 1; - mouseX = 0; - mouseY = 0; - oldMouseX = 0; - oldMouseY = 0; - } - return; - } - - // Peek at the mouse position - if (event instanceof TMouseEvent) { - TMouseEvent mouse = (TMouseEvent) event; - synchronized (getScreen()) { - if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) { - oldMouseX = mouseX; - oldMouseY = mouseY; - mouseX = mouse.getX(); - mouseY = mouse.getY(); + 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(); + return; } - } - // Put into the main queue - drainEventQueue.add(event); + // Put into the main queue + drainEventQueue.add(event); + } } /** @@ -989,6 +1076,14 @@ public class TApplication implements Runnable { // 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(); + } + // See if we need to switch focus to another window or the menu checkSwitchFocus((TMouseEvent) event); } @@ -1038,13 +1133,12 @@ public class TApplication implements Runnable { // shortcutted by the active window, and if so dispatch the menu // event. boolean windowWillShortcut = false; - for (TWindow window: windows) { - if (window.isActive()) { - if (window.isShortcutKeypress(keypress.getKey())) { - // We do not process this key, it will be passed to - // the window instead. - windowWillShortcut = true; - } + 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; } } @@ -1082,25 +1176,40 @@ public class TApplication implements Runnable { } // Dispatch events to the active window ------------------------------- - for (TWindow window: windows) { - if (window.isActive()) { - if (event instanceof TMouseEvent) { - TMouseEvent mouse = (TMouseEvent) event; - // Convert the mouse relative x/y to window coordinates - assert (mouse.getX() == mouse.getAbsoluteX()); - assert (mouse.getY() == mouse.getAbsoluteY()); - mouse.setX(mouse.getX() - window.getX()); - mouse.setY(mouse.getY() - window.getY()); - } - if (debugEvents) { - System.err.printf("TApplication dispatch event: %s\n", - event); + 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 (window.mouseWouldHit(mouse)) { + dispatchToDesktop = false; } - window.handleEvent(event); - break; + } else if (event instanceof TKeypressEvent) { + dispatchToDesktop = false; + } + + if (debugEvents) { + System.err.printf("TApplication dispatch event: %s\n", + event); + } + window.handleEvent(event); + } + if (dispatchToDesktop) { + // This event is fair game for the desktop to process. + if (desktop != null) { + desktop.handleEvent(event); } } } + /** * Dispatch one event to the appropriate widget or application-level * event handler. This is the secondary event handler used by certain @@ -1110,6 +1219,17 @@ public class TApplication implements Runnable { * @see #primaryHandleEvent(TInputEvent event) */ private void secondaryHandleEvent(final TInputEvent event) { + // 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(); + } + } + secondaryEventReceiver.handleEvent(event); } @@ -1119,12 +1239,18 @@ public class TApplication implements Runnable { * @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(); } @@ -1133,12 +1259,6 @@ public class TApplication implements Runnable { */ public final void yield() { assert (secondaryEventReceiver != null); - // This is where we handoff the event handler lock from the primary - // to secondary thread. We unlock here, and in a future loop the - // secondary thread locks again. When it gives up, we have the - // single lock back. - boolean oldLock = unlockHandleEvent(); - assert (oldLock); while (secondaryEventReceiver != null) { synchronized (primaryEventHandler) { @@ -1156,34 +1276,246 @@ public class TApplication implements Runnable { */ private void doIdle() { if (debugThreads) { - System.err.printf("doIdle()\n"); + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " doIdle()\n"); } - // Now run any timers that have timed out - Date now = new Date(); - List keepTimers = new LinkedList(); - for (TTimer timer: timers) { - if (timer.getNextTick().getTime() <= now.getTime()) { - timer.tick(); - if (timer.recurring) { + 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); } - } else { - keepTimers.add(timer); } + timers = keepTimers; } - timers = keepTimers; // Call onIdle's for (TWindow window: windows) { window.onIdle(); } + if (desktop != null) { + desktop.onIdle(); + } } // ------------------------------------------------------------------------ // TWindow management ----------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Return the total number of windows. + * + * @return the total number of windows + */ + public final int windowCount() { + return windows.size(); + } + + /** + * Return the number of windows that are showing. + * + * @return the number of windows that are showing on screen + */ + public final int shownWindowCount() { + int n = 0; + for (TWindow w: windows) { + if (w.isShown()) { + n++; + } + } + return n; + } + + /** + * Return the number of windows that are hidden. + * + * @return the number of windows that are hidden + */ + public final int hiddenWindowCount() { + int n = 0; + for (TWindow w: windows) { + if (w.isHidden()) { + n++; + } + } + return n; + } + + /** + * Check if a window instance is in this application's window list. + * + * @param window window to look for + * @return true if this window is in the list + */ + public final boolean hasWindow(final TWindow window) { + if (windows.size() == 0) { + return false; + } + for (TWindow w: windows) { + if (w == window) { + assert (window.getApplication() == this); + return true; + } + } + return false; + } + + /** + * Activate a window: bring it to the top and have it receive events. + * + * @param window the window to become the new active window + */ + public void activateWindow(final TWindow window) { + if (hasWindow(window) == false) { + /* + * Someone has a handle to a window I don't have. Ignore this + * request. + */ + return; + } + + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); + } + } + + assert (windows.size() > 0); + + if (window.isHidden()) { + // Unhiding will also activate. + showWindow(window); + return; + } + assert (window.isShown()); + + if (windows.size() == 1) { + assert (window == windows.get(0)); + if (activeWindow == null) { + activeWindow = window; + window.setZ(0); + activeWindow.setActive(true); + activeWindow.onFocus(); + } + + assert (window.isActive()); + assert (activeWindow == window); + return; + } + + if (activeWindow == window) { + assert (window.isActive()); + + // Window is already active, do nothing. + return; + } + + assert (!window.isActive()); + if (activeWindow != null) { + assert (activeWindow.getZ() == 0); + + activeWindow.setActive(false); + activeWindow.setZ(window.getZ()); + + // Unset activeWindow now before unfocus, so that a window + // lifecycle change inside onUnfocus() doesn't call + // switchWindow() and lead to a stack overflow. + TWindow oldActiveWindow = activeWindow; + activeWindow = null; + oldActiveWindow.onUnfocus(); + } + activeWindow = window; + activeWindow.setZ(0); + activeWindow.setActive(true); + activeWindow.onFocus(); + return; + } + + /** + * Hide a window. + * + * @param window the window to hide + */ + public void hideWindow(final TWindow window) { + if (hasWindow(window) == false) { + /* + * Someone has a handle to a window I don't have. Ignore this + * request. + */ + return; + } + + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); + } + } + + assert (windows.size() > 0); + + if (!window.hidden) { + if (window == activeWindow) { + if (shownWindowCount() > 1) { + switchWindow(true); + } else { + activeWindow = null; + window.setActive(false); + window.onUnfocus(); + } + } + window.hidden = true; + window.onHide(); + } + } + + /** + * Show a window. + * + * @param window the window to show + */ + public void showWindow(final TWindow window) { + if (hasWindow(window) == false) { + /* + * Someone has a handle to a window I don't have. Ignore this + * request. + */ + return; + } + + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); + } + } + + assert (windows.size() > 0); + + if (window.hidden) { + window.hidden = false; + window.onShow(); + activateWindow(window); + } + } + /** * Close window. Note that the window's destructor is NOT called by this * method, instead the GC is assumed to do the cleanup. @@ -1191,13 +1523,28 @@ public class TApplication implements Runnable { * @param window the window to remove */ public final void closeWindow(final TWindow window) { + if (hasWindow(window) == false) { + /* + * Someone has a handle to a window I don't have. Ignore this + * request. + */ + return; + } + synchronized (windows) { + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); + } + } + int z = window.getZ(); window.setZ(-1); window.onUnfocus(); Collections.sort(windows); windows.remove(0); - TWindow activeWindow = null; + activeWindow = null; for (TWindow w: windows) { if (w.getZ() > z) { w.setZ(w.getZ() - 1); @@ -1233,6 +1580,13 @@ public class TApplication implements Runnable { secondaryEventHandler.notify(); } } + + // Permit desktop to be active if it is the only thing left. + if (desktop != null) { + if (windows.size() == 0) { + desktop.setActive(true); + } + } } /** @@ -1242,45 +1596,55 @@ public class TApplication implements Runnable { * otherwise switch to the previous window in the list */ public final void switchWindow(final boolean forward) { - // Only switch if there are multiple windows - if (windows.size() < 2) { + // Only switch if there are multiple visible windows + if (shownWindowCount() < 2) { return; } + assert (activeWindow != null); synchronized (windows) { + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); + } + } // Swap z/active between active window and the next in the list int activeWindowI = -1; for (int i = 0; i < windows.size(); i++) { - if (windows.get(i).isActive()) { + if (windows.get(i) == activeWindow) { + assert (activeWindow.isActive()); activeWindowI = i; break; + } else { + assert (!windows.get(0).isActive()); } } assert (activeWindowI >= 0); // Do not switch if a window is modal - if (windows.get(activeWindowI).isModal()) { + if (activeWindow.isModal()) { return; } - int nextWindowI; - if (forward) { - nextWindowI = (activeWindowI + 1) % windows.size(); - } else { - if (activeWindowI == 0) { - nextWindowI = windows.size() - 1; + int nextWindowI = activeWindowI; + for (;;) { + if (forward) { + nextWindowI++; + nextWindowI %= windows.size(); } else { - nextWindowI = activeWindowI - 1; + nextWindowI--; + if (nextWindowI < 0) { + nextWindowI = windows.size() - 1; + } } - } - windows.get(activeWindowI).setActive(false); - windows.get(activeWindowI).setZ(windows.get(nextWindowI).getZ()); - windows.get(activeWindowI).onUnfocus(); - windows.get(nextWindowI).setZ(0); - windows.get(nextWindowI).setActive(true); - windows.get(nextWindowI).onFocus(); + if (windows.get(nextWindowI).isShown()) { + activateWindow(windows.get(nextWindowI)); + break; + } + } } // synchronized (windows) } @@ -1297,25 +1661,43 @@ public class TApplication implements Runnable { return; } + // Do not add the desktop to the window list. + if (window instanceof TDesktop) { + return; + } + synchronized (windows) { + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); + } + } + // Do not allow a modal window to spawn a non-modal window. If a // modal window is active, then this window will become modal // too. if (modalWindowActive()) { window.flags |= TWindow.MODAL; window.flags |= TWindow.CENTERED; + window.hidden = false; } - for (TWindow w: windows) { - if (w.isActive()) { - w.setActive(false); - w.onUnfocus(); + if (window.isShown()) { + for (TWindow w: windows) { + if (w.isActive()) { + w.setActive(false); + w.onUnfocus(); + } + w.setZ(w.getZ() + 1); } - w.setZ(w.getZ() + 1); } windows.add(window); - window.setZ(0); - window.setActive(true); - window.onFocus(); + if (window.isShown()) { + activeWindow = window; + activeWindow.setZ(0); + activeWindow.setActive(true); + activeWindow.onFocus(); + } if (((window.flags & TWindow.CENTERED) == 0) && smartWindowPlacement) { @@ -1323,6 +1705,11 @@ public class TApplication implements Runnable { doSmartPlacement(window); } } + + // Desktop cannot be active over any other window. + if (desktop != null) { + desktop.setActive(false); + } } /** @@ -1477,11 +1864,11 @@ public class TApplication implements Runnable { continue; } for (int x = w.getX(); x < w.getX() + w.getWidth(); x++) { - if (x == width) { + if (x >= width) { continue; } for (int y = w.getY(); y < w.getY() + w.getHeight(); y++) { - if (y == height) { + if (y >= height) { continue; } overlapMatrix[x][y]++; @@ -1524,11 +1911,11 @@ public class TApplication implements Runnable { long newOverlapN = 0; // Start by adding each new cell. for (int wx = x; wx < x + window.getWidth(); wx++) { - if (wx == width) { + if (wx >= width) { continue; } for (int wy = y; wy < y + window.getHeight(); wy++) { - if (wy == height) { + if (wy >= height) { continue; } newMatrix[wx][wy]++; @@ -1629,8 +2016,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); @@ -1657,8 +2044,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); @@ -1672,46 +2059,64 @@ public class TApplication implements Runnable { return; } - // Only switch if there are multiple windows - if (windows.size() < 2) { + // If a menu is still active, don't switch windows + if (activeMenu != null) { return; } - // Switch on the upclick - if (mouse.getType() != TMouseEvent.Type.MOUSE_UP) { + // Only switch if there are multiple windows + if (windows.size() < 2) { return; } - synchronized (windows) { - Collections.sort(windows); - if (windows.get(0).isModal()) { - // Modal windows don't switch - return; - } + if (((focusFollowsMouse == true) + && (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)) + || (mouse.getType() == TMouseEvent.Type.MOUSE_UP) + ) { + synchronized (windows) { + Collections.sort(windows); + if (windows.get(0).isModal()) { + // Modal windows don't switch + return; + } - for (TWindow window: windows) { - assert (!window.isModal()); - if (window.mouseWouldHit(mouse)) { - if (window == windows.get(0)) { - // Clicked on the same window, nothing to do - return; + for (TWindow window: windows) { + assert (!window.isModal()); + + if (window.isHidden()) { + assert (!window.isActive()); + continue; } - // We will be switching to another window - assert (windows.get(0).isActive()); - assert (!window.isActive()); - windows.get(0).onUnfocus(); - windows.get(0).setActive(false); - windows.get(0).setZ(window.getZ()); - window.setZ(0); - window.setActive(true); - window.onFocus(); - return; + if (window.mouseWouldHit(mouse)) { + if (window == windows.get(0)) { + // Clicked on the same window, nothing to do + assert (window.isActive()); + return; + } + + // We will be switching to another window + assert (windows.get(0).isActive()); + assert (windows.get(0) == activeWindow); + assert (!window.isActive()); + activeWindow.onUnfocus(); + activeWindow.setActive(false); + activeWindow.setZ(window.getZ()); + activeWindow = window; + window.setZ(0); + window.setActive(true); + window.onFocus(); + return; + } } } + + // Clicked on the background, nothing to do + return; } - // Clicked on the background, nothing to do + // Nothing to do: this isn't a mouse up, or focus isn't following + // mouse. return; } @@ -1729,6 +2134,53 @@ public class TApplication implements Runnable { } } + /** + * Get a (shallow) copy of the menu list. + * + * @return a copy of the menu list + */ + public final List getAllMenus() { + return new LinkedList(menus); + } + + /** + * Add a top-level menu to the list. + * + * @param menu the menu to add + * @throws IllegalArgumentException if the menu is already used in + * another TApplication + */ + public final void addMenu(final TMenu menu) { + if ((menu.getApplication() != null) + && (menu.getApplication() != this) + ) { + throw new IllegalArgumentException("Menu " + menu + " is already " + + "part of application " + menu.getApplication()); + } + closeMenu(); + menus.add(menu); + recomputeMenuX(); + } + + /** + * Remove a top-level menu from the list. + * + * @param menu the menu to remove + * @throws IllegalArgumentException if the menu is already used in + * another TApplication + */ + public final void removeMenu(final TMenu menu) { + if ((menu.getApplication() != null) + && (menu.getApplication() != this) + ) { + throw new IllegalArgumentException("Menu " + menu + " is already " + + "part of application " + menu.getApplication()); + } + closeMenu(); + menus.remove(menu); + recomputeMenuX(); + } + /** * Turn off a sub-menu. */ @@ -1854,7 +2306,14 @@ 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()); + } } } @@ -1864,10 +2323,17 @@ public class TApplication implements Runnable { * @param event new event to add to the queue */ public final void postMenuEvent(final TInputEvent event) { - synchronized (fillEventQueue) { - fillEventQueue.add(event); + synchronized (this) { + synchronized (fillEventQueue) { + fillEventQueue.add(event); + } + if (debugThreads) { + System.err.println(System.currentTimeMillis() + " " + + Thread.currentThread() + " postMenuEvent() wake up main"); + } + closeMenu(); + this.notify(); } - closeMenu(); } /** @@ -1900,14 +2366,14 @@ public class TApplication implements Runnable { * @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; } @@ -1917,14 +2383,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; } @@ -1934,7 +2400,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); @@ -1944,9 +2410,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; } @@ -1956,7 +2422,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); @@ -1965,8 +2431,9 @@ 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; } @@ -1984,9 +2451,10 @@ public class TApplication implements Runnable { protected boolean onCommand(final TCommandEvent command) { // Default: handle cmExit if (command.equals(cmExit)) { - if (messageBox("Confirmation", "Exit application?", + if (messageBox(i18n.getString("exitDialogTitle"), + i18n.getString("exitDialogText"), TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { - quit = true; + exit(); } return true; } @@ -2009,6 +2477,16 @@ public class TApplication implements Runnable { 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; } @@ -2023,9 +2501,10 @@ public class TApplication implements Runnable { // Default: handle MID_EXIT if (menu.getId() == TMenu.MID_EXIT) { - if (messageBox("Confirmation", "Exit application?", + if (messageBox(i18n.getString("exitDialogTitle"), + i18n.getString("exitDialogText"), TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { - quit = true; + exit(); } return true; } @@ -2051,6 +2530,10 @@ public class TApplication implements Runnable { showAboutDialog(); return true; } + if (menu.getId() == TMenu.MID_REPAINT) { + doRepaint(); + return true; + } return false; } @@ -2101,17 +2584,21 @@ public class TApplication implements Runnable { Date now = new Date(); long nowTime = now.getTime(); long sleepTime = timeout; - for (TTimer timer: timers) { - long nextTickTime = timer.getNextTick().getTime(); - if (nextTickTime < nowTime) { - return 0; - } - long timeDifference = nextTickTime - nowTime; - if (timeDifference < sleepTime) { - sleepTime = timeDifference; + synchronized (timers) { + for (TTimer timer: timers) { + long nextTickTime = timer.getNextTick().getTime(); + if (nextTickTime < nowTime) { + return 0; + } + + long timeDifference = nextTickTime - nowTime; + if (timeDifference < sleepTime) { + sleepTime = timeDifference; + } } } + assert (sleepTime >= 0); assert (sleepTime <= timeout); return sleepTime; @@ -2261,4 +2748,82 @@ public class TApplication implements Runnable { return box.getFilename(); } + /** + * Convenience function to create a new window and make it active. + * Window will be located at (0, 0). + * + * @param title window title, will be centered along the top border + * @param width width of window + * @param height height of window + */ + public final TWindow addWindow(final String title, final int width, + final int height) { + + TWindow window = new TWindow(this, title, 0, 0, width, height); + return window; + } + + /** + * Convenience function to create a new window and make it active. + * Window will be located at (0, 0). + * + * @param title window title, will be centered along the top border + * @param width width of window + * @param height height of window + * @param flags bitmask of RESIZABLE, CENTERED, or MODAL + */ + public final TWindow addWindow(final String title, + final int width, final int height, final int flags) { + + TWindow window = new TWindow(this, title, 0, 0, width, height, flags); + return window; + } + + /** + * Convenience function to create a new window and make it active. + * + * @param title window title, will be centered along the top border + * @param x column relative to parent + * @param y row relative to parent + * @param width width of window + * @param height height of window + */ + public final TWindow addWindow(final String title, + final int x, final int y, final int width, final int height) { + + TWindow window = new TWindow(this, title, x, y, width, height); + return window; + } + + /** + * Convenience function to create a new window and make it active. + * + * @param title window title, will be centered along the top border + * @param x column relative to parent + * @param y row relative to parent + * @param width width of window + * @param height height of window + * @param flags mask of RESIZABLE, CENTERED, or MODAL + */ + public final TWindow addWindow(final String title, + final int x, final int y, final int width, final int height, + final int flags) { + + TWindow window = new TWindow(this, title, x, y, width, height, flags); + return window; + } + + /** + * Convenience function to open a file in an editor window and make it + * active. + * + * @param file the file to open + * @throws IOException if a java.io operation throws + */ + public final TEditorWindow addEditor(final File file) throws IOException { + + TEditorWindow editor = new TEditorWindow(this, file); + return editor; + } + }