X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTApplication.java;h=8b436ab9a99ec9219b9755ffce379c62d834e10f;hb=e8a11f986bfe2556e450d7b8ad6ef0059b369bbc;hp=e710895b555c6d645f21cc06870a4e25afff0a7a;hpb=92554d64c21c6a477fd23a06ca3a64a542b622a3;p=nikiroo-utils.git diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index e710895..8b436ab 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -1,29 +1,27 @@ -/** +/* * Jexer - Java Text User Interface * - * License: LGPLv3 or later - * - * This module is licensed under the GNU Lesser General Public License - * Version 3. Please see the file "COPYING" in this directory for more - * information about the GNU Lesser General Public License Version 3. + * The MIT License (MIT) * - * Copyright (C) 2015 Kevin Lamonte + * Copyright (C) 2017 Kevin Lamonte * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation; either version 3 of - * the License, or (at your option) any later version. + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, see - * http://www.gnu.org/licenses/, or write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. * * @author Kevin Lamonte [kevin.lamonte@gmail.com] * @version 1 @@ -31,7 +29,10 @@ package jexer; 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.util.Collections; import java.util.Date; @@ -43,7 +44,6 @@ import java.util.Map; import jexer.bits.CellAttributes; import jexer.bits.ColorTheme; -import jexer.bits.GraphicsChars; import jexer.event.TCommandEvent; import jexer.event.TInputEvent; import jexer.event.TKeypressEvent; @@ -51,18 +51,25 @@ import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; import jexer.backend.Backend; -import jexer.backend.AWTBackend; +import jexer.backend.Screen; +import jexer.backend.SwingBackend; import jexer.backend.ECMA48Backend; -import jexer.io.Screen; +import jexer.backend.TWindowBackend; import jexer.menu.TMenu; import jexer.menu.TMenuItem; import 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 { +public class TApplication implements Runnable { + + // ------------------------------------------------------------------------ + // Public constants ------------------------------------------------------- + // ------------------------------------------------------------------------ /** * If true, emit thread stuff to System.err. @@ -74,6 +81,36 @@ public class TApplication { */ private static final boolean debugEvents = false; + /** + * If true, do "smart placement" on new windows that are not specified to + * be centered. + */ + private static final boolean smartWindowPlacement = true; + + /** + * Two backend types are available. + */ + public static enum BackendType { + /** + * A Swing JFrame. + */ + SWING, + + /** + * An ECMA48 / ANSI X3.64 / XTERM style terminal. + */ + ECMA48, + + /** + * Synonym for ECMA48. + */ + XTERM + } + + // ------------------------------------------------------------------------ + // Primary/secondary event handlers --------------------------------------- + // ------------------------------------------------------------------------ + /** * WidgetEventHandler is the main event consumer loop. There are at most * two such threads in existence: the primary for normal case and a @@ -122,17 +159,17 @@ public class TApplication { } synchronized (this) { - /* - System.err.printf("%s %s sleep\n", this, primary ? - "primary" : "secondary"); - */ + if (debugThreads) { + System.err.printf("%s %s sleep\n", this, + primary ? "primary" : "secondary"); + } this.wait(); - /* - System.err.printf("%s %s AWAKE\n", this, primary ? - "primary" : "secondary"); - */ + if (debugThreads) { + System.err.printf("%s %s AWAKE\n", this, + primary ? "primary" : "secondary"); + } if ((!primary) && (application.secondaryEventReceiver == null) @@ -141,13 +178,12 @@ public class TApplication { // got here then something went wrong with // the handoff between yield() and // closeWindow(). - - System.err.printf("secondary exiting at wrong time, why?\n"); synchronized (application.primaryEventHandler) { application.primaryEventHandler.notify(); } application.secondaryEventHandler = null; - return; + throw new RuntimeException( + "secondary exited at wrong time"); } break; } @@ -156,6 +192,11 @@ public class TApplication { } } + // Wait for drawAll() or doIdle() to be done, then handle the + // events. + boolean oldLock = lockHandleEvent(); + assert (oldLock == false); + // Pull all events off the queue for (;;) { TInputEvent event = null; @@ -165,16 +206,12 @@ public class TApplication { } event = application.drainEventQueue.remove(0); } - // Wait for drawAll() or doIdle() to be done, then handle - // the event. - boolean oldLock = lockHandleEvent(); - assert (oldLock == false); + application.repaint = true; if (primary) { primaryHandleEvent(event); } else { secondaryHandleEvent(event); } - application.repaint = true; if ((!primary) && (application.secondaryEventReceiver == null) ) { @@ -192,14 +229,14 @@ public class TApplication { // All done! return; - } else { - // Unlock. Either I am primary thread, or I am - // secondary thread and still running. - oldLock = unlockHandleEvent(); - assert (oldLock == true); } } // for (;;) + // Unlock. Either I am primary thread, or I am secondary + // thread and still running. + oldLock = unlockHandleEvent(); + assert (oldLock == true); + // I have done some work of some kind. Tell the main run() // loop to wake up now. synchronized (application) { @@ -263,7 +300,14 @@ public class TApplication { synchronized (this) { // Wait for TApplication.run() to finish using the global state // before allowing further event processing. - while (lockoutHandleEvent == true); + while (lockoutHandleEvent == true) { + try { + // Backoff so that the backend can finish its work. + Thread.sleep(5); + } catch (InterruptedException e) { + // SQUASH + } + } oldValue = insideHandleEvent; insideHandleEvent = true; @@ -312,7 +356,14 @@ public class TApplication { lockoutHandleEvent = true; // Wait for the last event to finish processing before returning // control to TApplication.run(). - while (insideHandleEvent == true); + 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"); @@ -331,18 +382,39 @@ public class TApplication { lockoutHandleEvent = false; } + // ------------------------------------------------------------------------ + // TApplication attributes ------------------------------------------------ + // ------------------------------------------------------------------------ + /** * Access to the physical screen, keyboard, and mouse. */ private Backend backend; + /** + * Get the Backend. + * + * @return the Backend + */ + public final Backend getBackend() { + return backend; + } + /** * Get the Screen. * * @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(); + } } /** @@ -355,6 +427,16 @@ public class TApplication { */ 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(). */ @@ -377,7 +459,7 @@ public class TApplication { private List subMenus; /** - * The currently acive menu. + * The currently active menu. */ private TMenu activeMenu = null; @@ -386,6 +468,11 @@ public class TApplication { */ private Map accelerators; + /** + * All menu items. + */ + private List menuItems; + /** * Windows and widgets pull colors from this ColorTheme. */ @@ -405,6 +492,11 @@ public class TApplication { */ private List windows; + /** + * The currently acive window. + */ + private TWindow activeWindow = null; + /** * Timers that are being ticked. */ @@ -420,12 +512,6 @@ public class TApplication { */ private volatile boolean repaint = true; - /** - * When true, just flush updates from the screen. This is only used to - * minimize physical writes for the mouse cursor. - */ - private boolean flush = false; - /** * 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 @@ -456,9 +542,133 @@ public class TApplication { 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 { + + switch (backendType) { + case SWING: + // The default SwingBackend is 80x25, 20 pt font. If you want to + // change that, you can pass the extra arguments to the + // SwingBackend constructor here. For example, if you wanted + // 90x30, 16 pt font: + // + // backend = new SwingBackend(this, 90, 30, 16); + backend = new SwingBackend(this); + break; + case XTERM: + // Fall through... + case ECMA48: + backend = new ECMA48Backend(this, null, null); + break; + default: + throw new IllegalArgumentException("Invalid backend type: " + + backendType); + } + TApplicationImpl(); + } + + /** + * Public constructor. The backend type will be BackendType.ECMA48. + * * @param input an InputStream connected to the remote user, or null for * System.in. If System.in is used, then on non-Windows systems it will * be put in raw mode; shutdown() will (blindly!) put System.in in cooked @@ -472,26 +682,60 @@ public class TApplication { public TApplication(final InputStream input, final OutputStream output) throws UnsupportedEncodingException { - // AWT is the default backend on Windows unless explicitly overridden - // by jexer.AWT. - boolean useAWT = false; - if (System.getProperty("os.name").startsWith("Windows")) { - useAWT = true; - } - if (System.getProperty("jexer.AWT") != null) { - if (System.getProperty("jexer.AWT", "false").equals("true")) { - useAWT = true; - } else { - useAWT = false; - } - } + backend = new ECMA48Backend(this, input, output); + TApplicationImpl(); + } + /** + * Public constructor. The backend type will be BackendType.ECMA48. + * + * @param input the InputStream underlying 'reader'. Its available() + * method is used to determine if reader.read() will block or not. + * @param reader a Reader connected to the remote user. + * @param writer a PrintWriter connected to the remote user. + * @param setRawMode if true, set System.in into raw mode with stty. + * This should in general not be used. It is here solely for Demo3, + * which uses System.in. + * @throws IllegalArgumentException if input, reader, or writer are null. + */ + public TApplication(final InputStream input, final Reader reader, + final PrintWriter writer, final boolean setRawMode) { + + backend = new ECMA48Backend(this, input, reader, writer, setRawMode); + TApplicationImpl(); + } - if (useAWT) { - backend = new AWTBackend(this); - } else { - backend = new ECMA48Backend(this, input, output); - } + /** + * Public constructor. The backend type will be BackendType.ECMA48. + * + * @param input the InputStream underlying 'reader'. Its available() + * method is used to determine if reader.read() will block or not. + * @param reader a Reader connected to the remote user. + * @param writer a PrintWriter connected to the remote user. + * @throws IllegalArgumentException if input, reader, or writer are null. + */ + public TApplication(final InputStream input, final Reader reader, + final PrintWriter writer) { + + this(input, reader, writer, false); + } + + /** + * Public constructor. This hook enables use with new non-Jexer + * backends. + * + * @param backend a Backend that is already ready to go. + */ + public TApplication(final Backend backend) { + this.backend = backend; + backend.setListener(this); + TApplicationImpl(); + } + + /** + * Finish construction once the backend is set. + */ + private void TApplicationImpl() { theme = new ColorTheme(); desktopBottom = getScreen().getHeight() - 1; fillEventQueue = new ArrayList(); @@ -501,39 +745,60 @@ public class TApplication { subMenus = new LinkedList(); 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(); } + // ------------------------------------------------------------------------ + // Screen refresh loop ---------------------------------------------------- + // ------------------------------------------------------------------------ + /** - * Invert the cell at the mouse pointer position. + * Invert the cell color at a position. This is used to track the mouse. + * + * @param x column position + * @param y row position */ - private void drawMouse() { - CellAttributes attr = getScreen().getAttrXY(mouseX, mouseY); + private void invertCell(final int x, final int y) { + if (debugThreads) { + System.err.printf("invertCell() %d %d\n", x, y); + } + CellAttributes attr = getScreen().getAttrXY(x, y); attr.setForeColor(attr.getForeColor().invert()); attr.setBackColor(attr.getBackColor().invert()); - getScreen().putAttrXY(mouseX, mouseY, attr, false); - flush = true; - - if (windows.size() == 0) { - repaint = true; - } + getScreen().putAttrXY(x, y, attr, false); } /** * Draw everything. */ - public final void drawAll() { + private void drawAll() { if (debugThreads) { System.err.printf("drawAll() enter\n"); } - if ((flush) && (!repaint)) { - backend.flushScreen(); - flush = false; - return; + if (!repaint) { + if (debugThreads) { + System.err.printf("drawAll() !repaint\n"); + } + synchronized (getScreen()) { + if ((oldMouseX != mouseX) || (oldMouseY != mouseY)) { + // The only thing that has happened is the mouse moved. + // Clear the old position and draw the new position. + invertCell(oldMouseX, oldMouseY); + invertCell(mouseX, mouseY); + oldMouseX = mouseX; + oldMouseY = mouseY; + } + if (getScreen().isDirty()) { + backend.flushScreen(); + } + return; + } } if (debugThreads) { @@ -546,16 +811,23 @@ public class TApplication { // 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 = 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 @@ -568,9 +840,10 @@ public class TApplication { for (TMenu menu: menus) { CellAttributes menuColor; CellAttributes menuMnemonicColor; - if (menu.getActive()) { + 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"); @@ -578,12 +851,12 @@ public class TApplication { // Draw the menu title getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ', menuColor); - getScreen().putStrXY(x + 1, 0, menu.getTitle(), 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.getActive()) { + if (menu.isActive()) { menu.drawChildren(); // Reset the screen clipping so we can draw the next title. getScreen().resetClipping(); @@ -597,14 +870,33 @@ public class TApplication { 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 - drawMouse(); + 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.visibleCursor()) { + if (activeWidget.isCursorVisible()) { getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(), activeWidget.getCursorAbsoluteY()); cursor = true; @@ -617,42 +909,43 @@ public class TApplication { } // Flush the screen contents - backend.flushScreen(); + if (getScreen().isDirty()) { + backend.flushScreen(); + } repaint = false; - flush = false; + } + + // ------------------------------------------------------------------------ + // Main loop -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Force this application to exit. + */ + public void exit() { + quit = true; } /** * Run this application until it exits. */ - public final void run() { + public void run() { while (!quit) { // Timeout is in milliseconds, so default timeout after 1 second // of inactivity. - long timeout = 0; + long timeout = 1000; // If I've got no updates to render, wait for something from the // backend or a timer. - if (!repaint && !flush) { - // Never sleep longer than 100 millis, to get windows with - // background tasks an opportunity to update the display. - timeout = getSleepTime(100); - - // See if there are any definitely events waiting to be - // processed. If so, do not wait -- either there is I/O - // coming in or the primary/secondary threads are still - // working. - synchronized (drainEventQueue) { - if (drainEventQueue.size() > 0) { - timeout = 0; - } - } - synchronized (fillEventQueue) { - if (fillEventQueue.size() > 0) { - timeout = 0; - } - } + 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) { @@ -661,6 +954,9 @@ public class TApplication { // wait until either the backend or the consumer threads have // something to do. try { + if (debugThreads) { + System.err.println("sleep " + timeout + " millis"); + } synchronized (this) { this.wait(timeout); } @@ -668,33 +964,30 @@ public class TApplication { // 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(); + // Pull any pending I/O events backend.getEvents(fillEventQueue); // Dispatch each event to the appropriate handler, one at a time. for (;;) { TInputEvent event = null; - synchronized (fillEventQueue) { - if (fillEventQueue.size() == 0) { - break; - } - event = fillEventQueue.remove(0); + if (fillEventQueue.size() == 0) { + break; } + event = fillEventQueue.remove(0); metaHandleEvent(event); } // Wake a consumer thread if we have any pending events. - synchronized (drainEventQueue) { - if (drainEventQueue.size() > 0) { - wakeEventHandler(); - } + if (drainEventQueue.size() > 0) { + wakeEventHandler(); } - // Prevent stepping on the primary or secondary event handler. - stopEventHandlers(); - // Process timers and call doIdle()'s doIdle(); @@ -762,29 +1055,24 @@ public class TApplication { // Screen resize if (event instanceof TResizeEvent) { TResizeEvent resize = (TResizeEvent) event; - getScreen().setDimensions(resize.getWidth(), - resize.getHeight()); - desktopBottom = getScreen().getHeight() - 1; - repaint = true; - mouseX = 0; - mouseY = 0; - return; - } - - // Peek at the mouse position - if (event instanceof TMouseEvent) { - TMouseEvent mouse = (TMouseEvent) event; - if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) { - mouseX = mouse.getX(); - mouseY = mouse.getY(); - drawMouse(); + 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); + } + return; } - // Put into the main queue - synchronized (drainEventQueue) { - drainEventQueue.add(event); - } + // Put into the main queue + drainEventQueue.add(event); } /** @@ -805,6 +1093,14 @@ public class TApplication { // 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); } @@ -822,11 +1118,11 @@ public class TApplication { break; } if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) - && (!mouse.getMouse1()) - && (!mouse.getMouse2()) - && (!mouse.getMouse3()) - && (!mouse.getMouseWheelUp()) - && (!mouse.getMouseWheelDown()) + && (!mouse.isMouse1()) + && (!mouse.isMouse2()) + && (!mouse.isMouse3()) + && (!mouse.isMouseWheelUp()) + && (!mouse.isMouseWheelDown()) ) { break; } @@ -850,18 +1146,33 @@ public class TApplication { if (event instanceof TKeypressEvent) { TKeypressEvent keypress = (TKeypressEvent) event; - // See if this key matches an accelerator, and if so dispatch the - // menu event. - TKeypress keypressLowercase = keypress.getKey().toLowerCase(); - TMenuItem item = null; - synchronized (accelerators) { - item = accelerators.get(keypressLowercase); + // See if this key matches an accelerator, and is not being + // shortcutted by the active window, and if so dispatch the menu + // event. + boolean windowWillShortcut = false; + if (activeWindow != null) { + assert (activeWindow.isShown()); + if (activeWindow.isShortcutKeypress(keypress.getKey())) { + // We do not process this key, it will be passed to the + // window instead. + windowWillShortcut = true; + } } - if (item != null) { - // Let the menu item dispatch - item.dispatch(); - return; - } else { + + if (!windowWillShortcut && !modalWindowActive()) { + TKeypress keypressLowercase = keypress.getKey().toLowerCase(); + TMenuItem item = null; + synchronized (accelerators) { + item = accelerators.get(keypressLowercase); + } + if (item != null) { + if (item.isEnabled()) { + // Let the menu item dispatch + item.dispatch(); + return; + } + } + // Handle the keypress if (onKeypress(keypress)) { return; @@ -882,25 +1193,40 @@ public class TApplication { } // Dispatch events to the active window ------------------------------- - for (TWindow window: windows) { - if (window.getActive()) { - 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 @@ -910,6 +1236,17 @@ public class TApplication { * @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); } @@ -921,13 +1258,11 @@ public class TApplication { public final void enableSecondaryEventReceiver(final TWidget widget) { assert (secondaryEventReceiver == null); assert (secondaryEventHandler == null); - assert (widget instanceof TMessageBox); + assert ((widget instanceof TMessageBox) + || (widget instanceof TFileOpenBox)); secondaryEventReceiver = widget; secondaryEventHandler = new WidgetEventHandler(this, false); (new Thread(secondaryEventHandler)).start(); - - // Refresh - repaint = true; } /** @@ -940,9 +1275,7 @@ public class TApplication { // secondary thread locks again. When it gives up, we have the // single lock back. boolean oldLock = unlockHandleEvent(); - assert (oldLock == true); - - // System.err.printf("YIELD\n"); + assert (oldLock); while (secondaryEventReceiver != null) { synchronized (primaryEventHandler) { @@ -953,8 +1286,6 @@ public class TApplication { } } } - - // System.err.printf("EXIT YIELD\n"); } /** @@ -984,55 +1315,222 @@ public class TApplication { for (TWindow window: windows) { window.onIdle(); } + if (desktop != null) { + desktop.onIdle(); + } } + // ------------------------------------------------------------------------ + // TWindow management ----------------------------------------------------- + // ------------------------------------------------------------------------ + /** - * Get the amount of time I can sleep before missing a Timer tick. + * Return the total number of windows. * - * @param timeout = initial (maximum) timeout in millis - * @return number of milliseconds between now and the next timer event + * @return the total number of windows */ - protected long getSleepTime(final long timeout) { - Date now = new Date(); - long nowTime = now.getTime(); - long sleepTime = timeout; - for (TTimer timer: timers) { - long nextTickTime = timer.getNextTick().getTime(); - if (nextTickTime < nowTime) { - return 0; - } + public final int windowCount() { + return windows.size(); + } - long timeDifference = nextTickTime - nowTime; - if (timeDifference < sleepTime) { - sleepTime = timeDifference; + /** + * 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++; } } - assert (sleepTime >= 0); - assert (sleepTime <= timeout); - return sleepTime; + return n; } /** - * Close window. Note that the window's destructor is NOT called by this - * method, instead the GC is assumed to do the cleanup. + * Return the number of windows that are hidden. * - * @param window the window to remove + * @return the number of windows that are hidden */ - public final void closeWindow(final TWindow window) { - int z = window.getZ(); - window.setZ(-1); - Collections.sort(windows); - windows.remove(0); - TWindow activeWindow = null; + public final int hiddenWindowCount() { + int n = 0; for (TWindow w: windows) { - if (w.getZ() > z) { - w.setZ(w.getZ() - 1); - if (w.getZ() == 0) { - w.setActive(true); - assert (activeWindow == null); - activeWindow = w; + 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; + } + + 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.onUnfocus(); + activeWindow.setActive(false); + activeWindow.setZ(window.getZ()); + } + 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; + } + + assert (windows.size() > 0); + + if (!window.hidden) { + if (window == activeWindow) { + if (shownWindowCount() > 1) { + switchWindow(true); } else { - w.setActive(false); + 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; + } + + 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. + * + * @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) { + int z = window.getZ(); + window.setZ(-1); + window.onUnfocus(); + Collections.sort(windows); + windows.remove(0); + activeWindow = null; + for (TWindow w: windows) { + if (w.getZ() > z) { + w.setZ(w.getZ() - 1); + if (w.getZ() == 0) { + w.setActive(true); + w.onFocus(); + assert (activeWindow == null); + activeWindow = w; + } else { + if (w.isActive()) { + w.setActive(false); + w.onUnfocus(); + } + } } } } @@ -1040,9 +1538,6 @@ public class TApplication { // Perform window cleanup window.onClose(); - // Refresh screen - repaint = true; - // Check if we are closing a TMessageBox or similar if (secondaryEventReceiver != null) { assert (secondaryEventHandler != null); @@ -1057,6 +1552,13 @@ public class TApplication { secondaryEventHandler.notify(); } } + + // Permit desktop to be active if it is the only thing left. + if (desktop != null) { + if (windows.size() == 0) { + desktop.setActive(true); + } + } } /** @@ -1066,43 +1568,51 @@ public class TApplication { * 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) { - // 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).getActive()) { - activeWindowI = i; - break; + // Swap z/active between active window and the next in the list + int activeWindowI = -1; + for (int i = 0; i < windows.size(); i++) { + if (windows.get(i) == activeWindow) { + assert (activeWindow.isActive()); + activeWindowI = i; + break; + } else { + assert (!windows.get(0).isActive()); + } } - } - assert (activeWindowI >= 0); + assert (activeWindowI >= 0); - // Do not switch if a window is modal - if (windows.get(activeWindowI).isModal()) { - return; - } + // Do not switch if a window is modal + if (activeWindow.isModal()) { + return; + } - int nextWindowI; - if (forward) { - nextWindowI = (activeWindowI + 1) % windows.size(); - } else { - if (activeWindowI == 0) { - nextWindowI = windows.size() - 1; - } else { - nextWindowI = activeWindowI - 1; + int nextWindowI = activeWindowI; + for (;;) { + if (forward) { + nextWindowI++; + nextWindowI %= windows.size(); + } else { + nextWindowI--; + if (nextWindowI < 0) { + nextWindowI = windows.size() - 1; + } + } + + if (windows.get(nextWindowI).isShown()) { + activateWindow(windows.get(nextWindowI)); + break; + } } - } - windows.get(activeWindowI).setActive(false); - windows.get(activeWindowI).setZ(windows.get(nextWindowI).getZ()); - windows.get(nextWindowI).setZ(0); - windows.get(nextWindowI).setActive(true); + } // synchronized (windows) - // Refresh - repaint = true; } /** @@ -1111,17 +1621,54 @@ public class TApplication { * @param window new window to add */ public final void addWindow(final TWindow window) { - // Do not allow a modal window to spawn a non-modal window - if ((windows.size() > 0) && (windows.get(0).isModal())) { - assert (window.isModal()); + + // Do not add menu windows to the window list. + if (window instanceof TMenu) { + return; } - for (TWindow w: windows) { - w.setActive(false); - w.setZ(w.getZ() + 1); + + // Do not add the desktop to the window list. + if (window instanceof TDesktop) { + return; + } + + synchronized (windows) { + // Do not allow a modal window to spawn a non-modal window. If a + // modal window is active, then this window will become modal + // too. + if (modalWindowActive()) { + window.flags |= TWindow.MODAL; + window.flags |= TWindow.CENTERED; + window.hidden = false; + } + if (window.isShown()) { + for (TWindow w: windows) { + if (w.isActive()) { + w.setActive(false); + w.onUnfocus(); + } + w.setZ(w.getZ() + 1); + } + } + windows.add(window); + if (window.isShown()) { + activeWindow = window; + activeWindow.setZ(0); + activeWindow.setActive(true); + activeWindow.onFocus(); + } + + if (((window.flags & TWindow.CENTERED) == 0) + && smartWindowPlacement) { + + doSmartPlacement(window); + } + } + + // Desktop cannot be active over any other window. + if (desktop != null) { + desktop.setActive(false); } - windows.add(window); - window.setActive(true); - window.setZ(0); } /** @@ -1133,9 +1680,244 @@ public class TApplication { if (windows.size() == 0) { return false; } - return windows.get(windows.size() - 1).isModal(); + + for (TWindow w: windows) { + if (w.isModal()) { + return true; + } + } + + return false; + } + + /** + * Close all open windows. + */ + private void closeAllWindows() { + // Don't do anything if we are in the menu + if (activeMenu != null) { + return; + } + while (windows.size() > 0) { + closeWindow(windows.get(0)); + } + } + + /** + * Re-layout the open windows as non-overlapping tiles. This produces + * almost the same results as Turbo Pascal 7.0's IDE. + */ + private void tileWindows() { + synchronized (windows) { + // Don't do anything if we are in the menu + if (activeMenu != null) { + return; + } + int z = windows.size(); + if (z == 0) { + return; + } + int a = 0; + int b = 0; + a = (int)(Math.sqrt(z)); + int c = 0; + while (c < a) { + b = (z - c) / a; + if (((a * b) + c) == z) { + break; + } + c++; + } + assert (a > 0); + assert (b > 0); + assert (c < a); + int newWidth = (getScreen().getWidth() / a); + int newHeight1 = ((getScreen().getHeight() - 1) / b); + int newHeight2 = ((getScreen().getHeight() - 1) / (b + c)); + + List sorted = new LinkedList(windows); + Collections.sort(sorted); + Collections.reverse(sorted); + for (int i = 0; i < sorted.size(); i++) { + int logicalX = i / b; + int logicalY = i % b; + if (i >= ((a - 1) * b)) { + logicalX = a - 1; + logicalY = i - ((a - 1) * b); + } + + TWindow w = sorted.get(i); + w.setX(logicalX * newWidth); + w.setWidth(newWidth); + if (i >= ((a - 1) * b)) { + w.setY((logicalY * newHeight2) + 1); + w.setHeight(newHeight2); + } else { + w.setY((logicalY * newHeight1) + 1); + w.setHeight(newHeight1); + } + } + } + } + + /** + * Re-layout the open windows as overlapping cascaded windows. + */ + private void cascadeWindows() { + synchronized (windows) { + // Don't do anything if we are in the menu + if (activeMenu != null) { + return; + } + int x = 0; + int y = 1; + List sorted = new LinkedList(windows); + Collections.sort(sorted); + Collections.reverse(sorted); + for (TWindow window: sorted) { + window.setX(x); + window.setY(y); + x++; + y++; + if (x > getScreen().getWidth()) { + x = 0; + } + if (y >= getScreen().getHeight()) { + y = 1; + } + } + } + } + + /** + * Place a window to minimize its overlap with other windows. + * + * @param window the window to place + */ + public final void doSmartPlacement(final TWindow window) { + // This is a pretty dumb algorithm, but seems to work. The hardest + // part is computing these "overlap" values seeking a minimum average + // overlap. + int xMin = 0; + int yMin = desktopTop; + int xMax = getScreen().getWidth() - window.getWidth() + 1; + int yMax = desktopBottom - window.getHeight() + 1; + if (xMax < xMin) { + xMax = xMin; + } + if (yMax < yMin) { + yMax = yMin; + } + + if ((xMin == xMax) && (yMin == yMax)) { + // No work to do, bail out. + return; + } + + // Compute the overlap matrix without the new window. + int width = getScreen().getWidth(); + int height = getScreen().getHeight(); + int overlapMatrix[][] = new int[width][height]; + for (TWindow w: windows) { + if (window == w) { + continue; + } + for (int x = w.getX(); x < w.getX() + w.getWidth(); x++) { + if (x >= width) { + continue; + } + for (int y = w.getY(); y < w.getY() + w.getHeight(); y++) { + if (y >= height) { + continue; + } + overlapMatrix[x][y]++; + } + } + } + + long oldOverlapTotal = 0; + long oldOverlapN = 0; + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + oldOverlapTotal += overlapMatrix[x][y]; + if (overlapMatrix[x][y] > 0) { + oldOverlapN++; + } + } + } + + + double oldOverlapAvg = (double) oldOverlapTotal / (double) oldOverlapN; + boolean first = true; + int windowX = window.getX(); + int windowY = window.getY(); + + // For each possible (x, y) position for the new window, compute a + // new overlap matrix. + for (int x = xMin; x < xMax; x++) { + for (int y = yMin; y < yMax; y++) { + + // Start with the matrix minus this window. + int newMatrix[][] = new int[width][height]; + for (int mx = 0; mx < width; mx++) { + for (int my = 0; my < height; my++) { + newMatrix[mx][my] = overlapMatrix[mx][my]; + } + } + + // Add this window's values to the new overlap matrix. + long newOverlapTotal = 0; + long newOverlapN = 0; + // Start by adding each new cell. + for (int wx = x; wx < x + window.getWidth(); wx++) { + if (wx >= width) { + continue; + } + for (int wy = y; wy < y + window.getHeight(); wy++) { + if (wy >= height) { + continue; + } + newMatrix[wx][wy]++; + } + } + // Now figure out the new value for total coverage. + for (int mx = 0; mx < width; mx++) { + for (int my = 0; my < height; my++) { + newOverlapTotal += newMatrix[x][y]; + if (newMatrix[mx][my] > 0) { + newOverlapN++; + } + } + } + double newOverlapAvg = (double) newOverlapTotal / (double) newOverlapN; + + if (first) { + // First time: just record what we got. + oldOverlapAvg = newOverlapAvg; + first = false; + } else { + // All other times: pick a new best (x, y) and save the + // overlap value. + if (newOverlapAvg < oldOverlapAvg) { + windowX = x; + windowY = y; + oldOverlapAvg = newOverlapAvg; + } + } + + } // for (int x = xMin; x < xMax; x++) + + } // for (int y = yMin; y < yMax; y++) + + // Finally, set the window's new coordinates. + window.setX(windowX); + window.setY(windowY); } + // ------------------------------------------------------------------------ + // TMenu management ------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Check if a mouse event would hit either the active menu or any open * sub-menus. @@ -1181,7 +1963,7 @@ public class TApplication { // See if they hit the menu bar if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) - && (mouse.getMouse1()) + && (mouse.isMouse1()) && (!modalWindowActive()) && (mouse.getAbsoluteY() == 0) ) { @@ -1203,13 +1985,12 @@ public class TApplication { menu.setActive(false); } } - repaint = true; return; } // See if they hit the menu bar if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) - && (mouse.getMouse1()) + && (mouse.isMouse1()) && (activeMenu != null) && (mouse.getAbsoluteY() == 0) ) { @@ -1234,47 +2015,67 @@ public class TApplication { // They switched menus oldMenu.setActive(false); } - repaint = true; - return; - } - - // Only switch if there are multiple windows - if (windows.size() < 2) { return; } - // Switch on the upclick - if (mouse.getType() != TMouseEvent.Type.MOUSE_UP) { + // If a menu is still active, don't switch windows + if (activeMenu != null) { return; } - Collections.sort(windows); - if (windows.get(0).isModal()) { - // Modal windows don't switch + // Only switch if there are multiple windows + if (windows.size() < 2) { 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 + 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; } - // We will be switching to another window - assert (windows.get(0).getActive()); - assert (!window.getActive()); - windows.get(0).setActive(false); - windows.get(0).setZ(window.getZ()); - window.setZ(0); - window.setActive(true); - repaint = true; - return; + for (TWindow window: windows) { + assert (!window.isModal()); + + if (window.isHidden()) { + assert (!window.isActive()); + continue; + } + + 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; } @@ -1290,7 +2091,53 @@ public class TApplication { } subMenus.clear(); } - repaint = true; + } + + /** + * 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(); } /** @@ -1302,7 +2149,6 @@ public class TApplication { assert (item != null); item.setActive(false); subMenus.remove(subMenus.size() - 1); - repaint = true; } /** @@ -1332,147 +2178,83 @@ public class TApplication { } activeMenu.setActive(false); activeMenu = menus.get(i); - activeMenu.setActive(true); - repaint = true; - return; - } - } - } - - /** - * Method that TApplication subclasses can override to handle menu or - * posted command events. - * - * @param command command event - * @return if true, this event was consumed - */ - protected boolean onCommand(final TCommandEvent command) { - // Default: handle cmExit - if (command.equals(cmExit)) { - if (messageBox("Confirmation", "Exit application?", - TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { - quit = true; - } - repaint = true; - return true; - } - - if (command.equals(cmShell)) { - openTerminal(0, 0, TWindow.RESIZABLE); - repaint = true; - return true; - } - - if (command.equals(cmTile)) { - tileWindows(); - repaint = true; - return true; - } - if (command.equals(cmCascade)) { - cascadeWindows(); - repaint = true; - return true; - } - if (command.equals(cmCloseAll)) { - closeAllWindows(); - repaint = true; - return true; + activeMenu.setActive(true); + return; + } } - - return false; } /** - * Method that TApplication subclasses can override to handle menu - * events. + * Add a menu item to the global list. If it has a keyboard accelerator, + * that will be added the global hash. * - * @param menu menu event - * @return if true, this event was consumed + * @param item the menu item */ - protected boolean onMenu(final TMenuEvent menu) { + public final void addMenuItem(final TMenuItem item) { + menuItems.add(item); - // Default: handle MID_EXIT - if (menu.getId() == TMenu.MID_EXIT) { - if (messageBox("Confirmation", "Exit application?", - TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { - quit = true; + TKeypress key = item.getKey(); + if (key != null) { + synchronized (accelerators) { + assert (accelerators.get(key) == null); + accelerators.put(key.toLowerCase(), item); } - // System.err.printf("onMenu MID_EXIT result: quit = %s\n", quit); - repaint = true; - return true; } + } - if (menu.getId() == TMenu.MID_SHELL) { - openTerminal(0, 0, TWindow.RESIZABLE); - repaint = true; - return true; + /** + * Disable one menu item. + * + * @param id the menu item ID + */ + public final void disableMenuItem(final int id) { + for (TMenuItem item: menuItems) { + if (item.getId() == id) { + item.setEnabled(false); + } } + } - if (menu.getId() == TMenu.MID_TILE) { - tileWindows(); - repaint = true; - return true; - } - if (menu.getId() == TMenu.MID_CASCADE) { - cascadeWindows(); - repaint = true; - return true; - } - if (menu.getId() == TMenu.MID_CLOSE_ALL) { - closeAllWindows(); - repaint = true; - return true; + /** + * Disable the range of menu items with ID's between lower and upper, + * inclusive. + * + * @param lower the lowest menu item ID + * @param upper the highest menu item ID + */ + public final void disableMenuItems(final int lower, final int upper) { + for (TMenuItem item: menuItems) { + if ((item.getId() >= lower) && (item.getId() <= upper)) { + item.setEnabled(false); + } } - return false; } /** - * Method that TApplication subclasses can override to handle keystrokes. + * Enable one menu item. * - * @param keypress keystroke event - * @return if true, this event was consumed + * @param id the menu item ID */ - protected boolean onKeypress(final TKeypressEvent keypress) { - // Default: only menu shortcuts - - // Process Alt-F, Alt-E, etc. menu shortcut keys - if (!keypress.getKey().getIsKey() - && keypress.getKey().getAlt() - && !keypress.getKey().getCtrl() - && (activeMenu == null) - ) { - - assert (subMenus.size() == 0); - - for (TMenu menu: menus) { - if (Character.toLowerCase(menu.getMnemonic().getShortcut()) - == Character.toLowerCase(keypress.getKey().getCh()) - ) { - activeMenu = menu; - menu.setActive(true); - repaint = true; - return true; - } + public final void enableMenuItem(final int id) { + for (TMenuItem item: menuItems) { + if (item.getId() == id) { + item.setEnabled(true); } } - - return false; } /** - * Add a keyboard accelerator to the global hash. + * Enable the range of menu items with ID's between lower and upper, + * inclusive. * - * @param item menu item this accelerator relates to - * @param keypress keypress that will dispatch a TMenuEvent + * @param lower the lowest menu item ID + * @param upper the highest menu item ID */ - public final void addAccelerator(final TMenuItem item, - final TKeypress keypress) { - - // System.err.printf("addAccelerator: key %s item %s\n", keypress, item); - - synchronized (accelerators) { - assert (accelerators.get(keypress) == null); - accelerators.put(keypress, item); + public final void enableMenuItems(final int lower, final int upper) { + for (TMenuItem item: menuItems) { + if ((item.getId() >= lower) && (item.getId() <= upper)) { + item.setEnabled(true); + } } } @@ -1492,7 +2274,7 @@ public class TApplication { * * @param event new event to add to the queue */ - public final void addMenuEvent(final TInputEvent event) { + public final void postMenuEvent(final TInputEvent event) { synchronized (fillEventQueue) { fillEventQueue.add(event); } @@ -1534,6 +2316,9 @@ public class TApplication { 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"); return fileMenu; } @@ -1548,6 +2333,9 @@ public class TApplication { 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"); return editMenu; } @@ -1567,102 +2355,177 @@ public class TApplication { 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"); return windowMenu; } /** - * Close all open windows. + * Convenience function to add a default "Help" menu. + * + * @return the new menu */ - private void closeAllWindows() { - // Don't do anything if we are in the menu - if (activeMenu != null) { - return; + public final TMenu addHelpMenu() { + TMenu helpMenu = addMenu("&Help"); + helpMenu.addDefaultItem(TMenu.MID_HELP_CONTENTS); + helpMenu.addDefaultItem(TMenu.MID_HELP_INDEX); + helpMenu.addDefaultItem(TMenu.MID_HELP_SEARCH); + helpMenu.addDefaultItem(TMenu.MID_HELP_PREVIOUS); + helpMenu.addDefaultItem(TMenu.MID_HELP_HELP); + helpMenu.addDefaultItem(TMenu.MID_HELP_ACTIVE_FILE); + helpMenu.addSeparator(); + helpMenu.addDefaultItem(TMenu.MID_ABOUT); + TStatusBar statusBar = helpMenu.newStatusBar("Access online help"); + statusBar.addShortcutKeypress(kbF1, cmHelp, "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) { + quit = true; + } + return true; } - for (TWindow window: windows) { - closeWindow(window); + + if (command.equals(cmShell)) { + openTerminal(0, 0, TWindow.RESIZABLE); + return true; } + + if (command.equals(cmTile)) { + tileWindows(); + return true; + } + if (command.equals(cmCascade)) { + cascadeWindows(); + return true; + } + if (command.equals(cmCloseAll)) { + closeAllWindows(); + return true; + } + + return false; } /** - * Re-layout the open windows as non-overlapping tiles. This produces - * almost the same results as Turbo Pascal 7.0's IDE. + * Method that TApplication subclasses can override to handle menu + * events. + * + * @param menu menu event + * @return if true, this event was consumed */ - private void tileWindows() { - // Don't do anything if we are in the menu - if (activeMenu != null) { - return; + protected boolean onMenu(final TMenuEvent menu) { + + // Default: handle MID_EXIT + if (menu.getId() == TMenu.MID_EXIT) { + if (messageBox("Confirmation", "Exit application?", + TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { + quit = true; + } + return true; } - int z = windows.size(); - if (z == 0) { - return; + + if (menu.getId() == TMenu.MID_SHELL) { + openTerminal(0, 0, TWindow.RESIZABLE); + return true; } - int a = 0; - int b = 0; - a = (int)(Math.sqrt(z)); - int c = 0; - while (c < a) { - b = (z - c) / a; - if (((a * b) + c) == z) { - break; - } - c++; + + 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; } - assert (a > 0); - assert (b > 0); - assert (c < a); - int newWidth = (getScreen().getWidth() / a); - int newHeight1 = ((getScreen().getHeight() - 1) / b); - int newHeight2 = ((getScreen().getHeight() - 1) / (b + c)); + return false; + } - List sorted = new LinkedList(windows); - Collections.sort(sorted); - Collections.reverse(sorted); - for (int i = 0; i < sorted.size(); i++) { - int logicalX = i / b; - int logicalY = i % b; - if (i >= ((a - 1) * b)) { - logicalX = a - 1; - logicalY = i - ((a - 1) * b); - } - - TWindow w = sorted.get(i); - w.setX(logicalX * newWidth); - w.setWidth(newWidth); - if (i >= ((a - 1) * b)) { - w.setY((logicalY * newHeight2) + 1); - w.setHeight(newHeight2); - } else { - w.setY((logicalY * newHeight1) + 1); - w.setHeight(newHeight1); + /** + * 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 ------------------------------------------------------ + // ------------------------------------------------------------------------ + /** - * Re-layout the open windows as overlapping cascaded windows. + * Get the amount of time I can sleep before missing a Timer tick. + * + * @param timeout = initial (maximum) timeout in millis + * @return number of milliseconds between now and the next timer event */ - private void cascadeWindows() { - // Don't do anything if we are in the menu - if (activeMenu != null) { - return; - } - int x = 0; - int y = 1; - List sorted = new LinkedList(windows); - Collections.sort(sorted); - Collections.reverse(sorted); - for (TWindow window: sorted) { - window.setX(x); - window.setY(y); - x++; - y++; - if (x > getScreen().getWidth()) { - x = 0; + private long getSleepTime(final long timeout) { + Date now = new Date(); + long nowTime = now.getTime(); + long sleepTime = timeout; + for (TTimer timer: timers) { + long nextTickTime = timer.getNextTick().getTime(); + if (nextTickTime < nowTime) { + return 0; } - if (y >= getScreen().getHeight()) { - y = 1; + + long timeDifference = nextTickTime - nowTime; + if (timeDifference < sleepTime) { + sleepTime = timeDifference; } } + assert (sleepTime >= 0); + assert (sleepTime <= timeout); + return sleepTime; } /** @@ -1694,6 +2557,10 @@ public class TApplication { } } + // ------------------------------------------------------------------------ + // Other TWindow constructors --------------------------------------------- + // ------------------------------------------------------------------------ + /** * Convenience function to spawn a message box. * @@ -1777,4 +2644,96 @@ public class TApplication { return new TTerminalWindow(this, x, y, flags); } + /** + * Convenience function to spawn an file open box. + * + * @param path path of selected file + * @return the result of the new file open box + * @throws IOException if java.io operation throws + */ + public final String fileOpenBox(final String path) throws IOException { + + TFileOpenBox box = new TFileOpenBox(this, path, TFileOpenBox.Type.OPEN); + return box.getFilename(); + } + + /** + * Convenience function to spawn an file open box. + * + * @param path path of selected file + * @param type one of the Type constants + * @return the result of the new file open box + * @throws IOException if java.io operation throws + */ + public final String fileOpenBox(final String path, + final TFileOpenBox.Type type) throws IOException { + + TFileOpenBox box = new TFileOpenBox(this, path, type); + 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; + } + }