X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTApplication.java;h=35aa32d0d0f5dd4c892ab4a3ae79db949b90effe;hb=2b9c27db318b916730aa04f2b41bd3bff795a5dc;hp=d4a6610834fa2994372ae3700efdf76a3c633718;hpb=48e27807150e00bc9a92844382ebc8cedf1d265f;p=nikiroo-utils.git diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index d4a6610..35aa32d 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -33,6 +33,7 @@ package jexer; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; +import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -42,11 +43,14 @@ import jexer.bits.GraphicsChars; import jexer.event.TCommandEvent; import jexer.event.TInputEvent; import jexer.event.TKeypressEvent; +import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; import jexer.backend.Backend; import jexer.backend.ECMA48Backend; import jexer.io.Screen; +import jexer.menu.TMenu; +import jexer.menu.TMenuItem; import static jexer.TCommand.*; import static jexer.TKeypress.*; @@ -80,9 +84,30 @@ public class TApplication { private int mouseY; /** - * Event queue that will be drained by either primary or secondary Fiber. + * Event queue that is filled by run(). */ - private List eventQueue; + private List fillEventQueue; + + /** + * Event queue that will be drained by either primary or secondary + * Thread. + */ + private List drainEventQueue; + + /** + * Top-level menus in this application. + */ + private List menus; + + /** + * Stack of activated sub-menus in this application. + */ + private List subMenus; + + /** + * The currently acive menu. + */ + private TMenu activeMenu = null; /** * Windows and widgets pull colors from this ColorTheme. @@ -98,6 +123,11 @@ public class TApplication { return theme; } + /** + * The top-level windows (but not menus). + */ + private List windows; + /** * When true, exit the application. */ @@ -111,7 +141,7 @@ public class TApplication { /** * Request full repaint on next screen refresh. */ - public void setRepaint() { + public final void setRepaint() { repaint = true; } @@ -166,30 +196,29 @@ public class TApplication { public TApplication(final InputStream input, final OutputStream output) throws UnsupportedEncodingException { - backend = new ECMA48Backend(input, output); - theme = new ColorTheme(); - desktopBottom = backend.getScreen().getHeight() - 1; - eventQueue = new LinkedList(); + backend = new ECMA48Backend(input, output); + theme = new ColorTheme(); + desktopBottom = getScreen().getHeight() - 1; + fillEventQueue = new LinkedList(); + drainEventQueue = new LinkedList(); + windows = new LinkedList(); + menus = new LinkedList(); + subMenus = new LinkedList(); } /** * Invert the cell at the mouse pointer position. */ private void drawMouse() { - CellAttributes attr = backend.getScreen().getAttrXY(mouseX, mouseY); + CellAttributes attr = getScreen().getAttrXY(mouseX, mouseY); attr.setForeColor(attr.getForeColor().invert()); attr.setBackColor(attr.getBackColor().invert()); - backend.getScreen().putAttrXY(mouseX, mouseY, attr, false); + getScreen().putAttrXY(mouseX, mouseY, attr, false); flush = true; - /* - if (windows.length == 0) { + if (windows.size() == 0) { repaint = true; } - */ - // TODO: remove this repaint after the above if (windows.length == 0) - // can be used again. - repaint = true; } /** @@ -210,31 +239,31 @@ public class TApplication { boolean cursor = false; // Start with a clean screen - backend.getScreen().clear(); + getScreen().clear(); // Draw the background CellAttributes background = theme.getColor("tapplication.background"); - backend.getScreen().putAll(GraphicsChars.HATCH, background); + getScreen().putAll(GraphicsChars.HATCH, background); - /* // Draw each window in reverse Z order - TWindow [] sorted = windows.dup; - sorted.sort.reverse; - foreach (w; sorted) { - w.drawChildren(); + List sorted = new LinkedList(windows); + Collections.sort(sorted); + Collections.reverse(sorted); + for (TWindow window: sorted) { + window.drawChildren(); } // Draw the blank menubar line - reset the screen clipping first so // it won't trim it out. - backend.getScreen().resetClipping(); - backend.getScreen().hLineXY(0, 0, backend.getScreen().getWidth(), ' ', + getScreen().resetClipping(); + getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ', theme.getColor("tmenu")); // Now draw the menus. int x = 1; - foreach (m; menus) { + for (TMenu menu: menus) { CellAttributes menuColor; CellAttributes menuMnemonicColor; - if (m.active) { + if (menu.getActive()) { menuColor = theme.getColor("tmenu.highlighted"); menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted"); } else { @@ -242,48 +271,45 @@ public class TApplication { menuMnemonicColor = theme.getColor("tmenu.mnemonic"); } // Draw the menu title - backend.getScreen().hLineXY(x, 0, cast(int)m.title.length + 2, ' ', + getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ', menuColor); - backend.getScreen().putStrXY(x + 1, 0, m.title, menuColor); + getScreen().putStrXY(x + 1, 0, menu.getTitle(), menuColor); // Draw the highlight character - backend.getScreen().putCharXY(x + 1 + m.mnemonic.shortcutIdx, 0, - m.mnemonic.shortcut, menuMnemonicColor); + getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(), + 0, menu.getMnemonic().getShortcut(), menuMnemonicColor); - if (m.active) { - m.drawChildren(); + if (menu.getActive()) { + menu.drawChildren(); // Reset the screen clipping so we can draw the next title. - backend.getScreen().resetClipping(); + getScreen().resetClipping(); } - x += m.title.length + 2; + x += menu.getTitle().length() + 2; } - foreach (m; subMenus) { + for (TMenu menu: subMenus) { // Reset the screen clipping so we can draw the next sub-menu. - backend.getScreen().resetClipping(); - m.drawChildren(); + getScreen().resetClipping(); + menu.drawChildren(); } - */ // Draw the mouse pointer drawMouse(); - /* // Place the cursor if it is visible TWidget activeWidget = null; - if (sorted.length > 0) { - activeWidget = sorted[$ - 1].getActiveChild(); - if (activeWidget.hasCursor) { - backend.getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(), + if (sorted.size() > 0) { + activeWidget = sorted.get(sorted.size() - 1).getActiveChild(); + if (activeWidget.visibleCursor()) { + getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(), activeWidget.getCursorAbsoluteY()); cursor = true; } } // Kill the cursor - if (cursor == false) { - backend.getScreen().hideCursor(); + if (!cursor) { + getScreen().hideCursor(); } - */ // Flush the screen contents backend.flushScreen(); @@ -296,23 +322,39 @@ public class TApplication { * Run this application until it exits. */ public final void run() { - List events = new LinkedList(); - while (!quit) { // Timeout is in milliseconds, so default timeout after 1 second // of inactivity. int timeout = getSleepTime(1000); - if (eventQueue.size() > 0) { - // Do not wait if there are definitely events waiting to be - // processed or a screen redraw to do. - timeout = 0; + // See if there are any definitely events waiting to be processed + // or a screen redraw to do. If so, do not wait if there is no + // I/O coming in. + synchronized (drainEventQueue) { + if (drainEventQueue.size() > 0) { + timeout = 0; + } + } + synchronized (fillEventQueue) { + if (fillEventQueue.size() > 0) { + timeout = 0; + } } - // Pull any pending input events - backend.getEvents(events, timeout); - metaHandleEvents(events); - events.clear(); + // Pull any pending I/O events + backend.getEvents(fillEventQueue, timeout); + + // 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); + } + metaHandleEvent(event); + } // Process timers and call doIdle()'s doIdle(); @@ -345,90 +387,210 @@ public class TApplication { /** * Peek at certain application-level events, add to eventQueue, and wake - * up the consuming Fiber. + * up the consuming Thread. * - * @param events the input events to consume + * @param event the input event to consume */ - private void metaHandleEvents(final List events) { + private void metaHandleEvent(final TInputEvent event) { - for (TInputEvent event: events) { + /* + System.err.printf(String.format("metaHandleEvents event: %s\n", + event)); System.err.flush(); + */ - /* - System.err.printf(String.format("metaHandleEvents event: %s\n", - event)); System.err.flush(); - */ + if (quit) { + // Do no more processing if the application is already trying + // to exit. + return; + } - if (quit) { - // Do no more processing if the application is already trying - // to exit. + // DEBUG + if (event instanceof TKeypressEvent) { + TKeypressEvent keypress = (TKeypressEvent) event; + if (keypress.equals(kbAltX)) { + quit = true; return; } + } + // DEBUG - // DEBUG - if (event instanceof TKeypressEvent) { - TKeypressEvent keypress = (TKeypressEvent) event; - if (keypress.equals(kbAltX)) { - quit = true; - return; - } + // Special application-wide events ------------------------------- + + // Abort everything + if (event instanceof TCommandEvent) { + TCommandEvent command = (TCommandEvent) event; + if (command.getCmd().equals(cmAbort)) { + quit = true; + return; } - // DEBUG + } - // Special application-wide events ------------------------------- + // 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; + } - // Abort everything - if (event instanceof TCommandEvent) { - TCommandEvent command = (TCommandEvent) event; - if (command.getCmd().equals(cmAbort)) { - quit = true; - 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(); } + } - // Screen resize - if (event instanceof TResizeEvent) { - TResizeEvent resize = (TResizeEvent) event; - backend.getScreen().setDimensions(resize.getWidth(), - resize.getHeight()); - desktopBottom = backend.getScreen().getHeight() - 1; - repaint = true; - mouseX = 0; - mouseY = 0; - continue; - } + // TODO: change to two separate threads + primaryHandleEvent(event); + + /* + + // Put into the main queue + addEvent(event); + + // Have one of the two consumer Fibers peel the events off + // the queue. + if (secondaryEventFiber !is null) { + assert(secondaryEventFiber.state == Fiber.State.HOLD); + + // Wake up the secondary handler for these events + secondaryEventFiber.call(); + } else { + assert(primaryEventFiber.state == Fiber.State.HOLD); + + // Wake up the primary handler for these events + primaryEventFiber.call(); + } + */ + + } + + /** + * Dispatch one event to the appropriate widget or application-level + * event handler. This is the primary event handler, it has the normal + * application-wide event handling. + * + * @param event the input event to consume + * @see #secondaryHandleEvent(TInputEvent event) + */ + private void primaryHandleEvent(final TInputEvent event) { + + // System.err.printf("Handle event: %s\n", event); + + // Special application-wide events ----------------------------------- + + // Peek at the mouse position + if (event instanceof TMouseEvent) { + // See if we need to switch focus to another window or the menu + checkSwitchFocus((TMouseEvent) event); + } + + // Handle menu events + if ((activeMenu != null) && !(event instanceof TCommandEvent)) { + TMenu menu = activeMenu; - // 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(); - } - } - /* + while (subMenus.size() > 0) { + TMenu subMenu = subMenus.get(subMenus.size() - 1); + if (subMenu.mouseWouldHit(mouse)) { + break; + } + if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) + && (!mouse.getMouse1()) + && (!mouse.getMouse2()) + && (!mouse.getMouse3()) + && (!mouse.getMouseWheelUp()) + && (!mouse.getMouseWheelDown()) + ) { + break; + } + // We navigated away from a sub-menu, so close it + closeSubMenu(); + } - // Put into the main queue - addEvent(event); + // Convert the mouse relative x/y to menu coordinates + assert (mouse.getX() == mouse.getAbsoluteX()); + assert (mouse.getY() == mouse.getAbsoluteY()); + if (subMenus.size() > 0) { + menu = subMenus.get(subMenus.size() - 1); + } + mouse.setX(mouse.getX() - menu.getX()); + mouse.setY(mouse.getY() - menu.getY()); + } + menu.handleEvent(event); + return; + } - // Have one of the two consumer Fibers peel the events off - // the queue. - if (secondaryEventFiber !is null) { - assert(secondaryEventFiber.state == Fiber.State.HOLD); + /* + TODO - // Wake up the secondary handler for these events - secondaryEventFiber.call(); + 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 = accelerators.get(keypressLowercase); + if (item != null) { + // Let the menu item dispatch + item.dispatch(); + return; } else { - assert(primaryEventFiber.state == Fiber.State.HOLD); + // Handle the keypress + if (onKeypress(keypress)) { + return; + } + } + } + */ - // Wake up the primary handler for these events - primaryEventFiber.call(); + if (event instanceof TCommandEvent) { + if (onCommand((TCommandEvent) event)) { + return; } - */ + } - } // for (TInputEvent event: events) + if (event instanceof TMenuEvent) { + if (onMenu((TMenuEvent) event)) { + return; + } + } + // 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()); + } + // System.err("TApplication dispatch event: %s\n", event); + window.handleEvent(event); + break; + } + } + } + /** + * Dispatch one event to the appropriate widget or application-level + * event handler. This is the secondary event handler used by certain + * special dialogs (currently TMessageBox and TFileOpenBox). + * + * @param event the input event to consume + * @see #primaryHandleEvent(TInputEvent event) + */ + private void secondaryHandleEvent(final TInputEvent event) { + // TODO } /** @@ -436,6 +598,7 @@ public class TApplication { */ private void doIdle() { /* + TODO // Now run any timers that have timed out auto now = Clock.currTime; TTimer [] keepTimers; @@ -492,23 +655,20 @@ public class TApplication { * @param window the window to remove */ public final void closeWindow(final TWindow window) { - /* - TODO - - uint z = window.z; - window.z = -1; - windows.sort; - windows = windows[1 .. $]; + int z = window.getZ(); + window.setZ(-1); + Collections.sort(windows); + windows.remove(0); TWindow activeWindow = null; - foreach (w; windows) { - if (w.z > z) { - w.z--; - if (w.z == 0) { - w.active = true; - assert(activeWindow is null); + 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; } else { - w.active = false; + w.setActive(false); } } } @@ -519,6 +679,10 @@ public class TApplication { // Refresh screen repaint = true; + /* + TODO + + // Check if we are closing a TMessageBox or similar if (secondaryEventReceiver !is null) { assert(secondaryEventFiber !is null); @@ -550,48 +714,43 @@ public class TApplication { * otherwise switch to the previous window in the list */ public final void switchWindow(final boolean forward) { - /* - TODO - // Only switch if there are multiple windows - if (windows.length < 2) { + if (windows.size() < 2) { return; } - // Swap z/active between active window and the next in the - // list - ptrdiff_t activeWindowI = -1; - for (auto i = 0; i < windows.length; i++) { - if (windows[i].active) { + // 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; } } - assert(activeWindowI >= 0); + assert (activeWindowI >= 0); // Do not switch if a window is modal - if (windows[activeWindowI].isModal()) { + if (windows.get(activeWindowI).isModal()) { return; } - size_t nextWindowI; + int nextWindowI; if (forward) { - nextWindowI = (activeWindowI + 1) % windows.length; + nextWindowI = (activeWindowI + 1) % windows.size(); } else { if (activeWindowI == 0) { - nextWindowI = windows.length - 1; + nextWindowI = windows.size() - 1; } else { nextWindowI = activeWindowI - 1; } } - windows[activeWindowI].active = false; - windows[activeWindowI].z = windows[nextWindowI].z; - windows[nextWindowI].z = 0; - windows[nextWindowI].active = true; + windows.get(activeWindowI).setActive(false); + windows.get(activeWindowI).setZ(windows.get(nextWindowI).getZ()); + windows.get(nextWindowI).setZ(0); + windows.get(nextWindowI).setActive(true); // Refresh repaint = true; - */ } /** @@ -600,21 +759,569 @@ 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()); + } + for (TWindow w: windows) { + w.setActive(false); + w.setZ(w.getZ() + 1); + } + windows.add(window); + window.setActive(true); + window.setZ(0); + } + + /** + * Check if there is a system-modal window on top. + * + * @return true if the active window is modal + */ + private boolean modalWindowActive() { + if (windows.size() == 0) { + return false; + } + return windows.get(windows.size() - 1).isModal(); + } + + /** + * Check if a mouse event would hit either the active menu or any open + * sub-menus. + * + * @param mouse mouse event + * @return true if the mouse would hit the active menu or an open + * sub-menu + */ + private boolean mouseOnMenu(final TMouseEvent mouse) { + assert (activeMenu != null); + List menus = new LinkedList(subMenus); + Collections.reverse(menus); + for (TMenu menu: menus) { + if (menu.mouseWouldHit(mouse)) { + return true; + } + } + return activeMenu.mouseWouldHit(mouse); + } + + /** + * See if we need to switch window or activate the menu based on + * a mouse click. + * + * @param mouse mouse event + */ + private void checkSwitchFocus(final TMouseEvent mouse) { + + if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) + && (activeMenu != null) + && (mouse.getAbsoluteY() != 0) + && (!mouseOnMenu(mouse)) + ) { + // They clicked outside the active menu, turn it off + activeMenu.setActive(false); + activeMenu = null; + for (TMenu menu: subMenus) { + menu.setActive(false); + } + subMenus.clear(); + // Continue checks + } + + // See if they hit the menu bar + if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) + && (mouse.getMouse1()) + && (!modalWindowActive()) + && (mouse.getAbsoluteY() == 0) + ) { + + for (TMenu menu: subMenus) { + menu.setActive(false); + } + subMenus.clear(); + + // They selected the menu, go activate it + for (TMenu menu: menus) { + if ((mouse.getAbsoluteX() >= menu.getX()) + && (mouse.getAbsoluteX() < menu.getX() + + menu.getTitle().length() + 2) + ) { + menu.setActive(true); + activeMenu = menu; + } else { + menu.setActive(false); + } + } + repaint = true; + return; + } + + // See if they hit the menu bar + if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) + && (mouse.getMouse1()) + && (activeMenu != null) + && (mouse.getAbsoluteY() == 0) + ) { + + TMenu oldMenu = activeMenu; + for (TMenu menu: subMenus) { + menu.setActive(false); + } + subMenus.clear(); + + // See if we should switch menus + for (TMenu menu: menus) { + if ((mouse.getAbsoluteX() >= menu.getX()) + && (mouse.getAbsoluteX() < menu.getX() + + menu.getTitle().length() + 2) + ) { + menu.setActive(true); + activeMenu = menu; + } + } + if (oldMenu != activeMenu) { + // 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) { + return; + } + + 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; + } + + // 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; + } + } + + // Clicked on the background, nothing to do + return; + } + + /** + * Turn off the menu. + */ + public final void closeMenu() { + if (activeMenu != null) { + activeMenu.setActive(false); + activeMenu = null; + for (TMenu menu: subMenus) { + menu.setActive(false); + } + subMenus.clear(); + } + repaint = true; + } + + /** + * Turn off a sub-menu. + */ + public final void closeSubMenu() { + assert (activeMenu != null); + TMenu item = subMenus.get(subMenus.size() - 1); + assert (item != null); + item.setActive(false); + subMenus.remove(subMenus.size() - 1); + repaint = true; + } + + /** + * Switch to the next menu. + * + * @param forward if true, then switch to the next menu in the list, + * otherwise switch to the previous menu in the list + */ + public final void switchMenu(final boolean forward) { + assert (activeMenu != null); + + for (TMenu menu: subMenus) { + menu.setActive(false); + } + subMenus.clear(); + + for (int i = 0; i < menus.size(); i++) { + if (activeMenu == menus.get(i)) { + if (forward) { + if (i < menus.size() - 1) { + i++; + } + } else { + if (i > 0) { + i--; + } + } + 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) { /* TODO - // Do not allow a modal window to spawn a non-modal window - if ((windows.length > 0) && (windows[0].isModal())) { - assert(window.isModal()); + // Default: handle cmExit + if (command.equals(cmExit)) { + if (messageBox("Confirmation", "Exit application?", + TMessageBox.Type.YESNO).result == TMessageBox.Result.YES) { + quit = true; + } + repaint = true; + return true; } - foreach (w; windows) { - w.active = false; - w.z++; + + if (command.equals(cmShell)) { + openTerminal(0, 0, TWindow.Flag.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; + } + */ + return false; + } + + /** + * Method that TApplication subclasses can override to handle menu + * events. + * + * @param menu menu event + * @return if true, this event was consumed + */ + protected boolean onMenu(final TMenuEvent menu) { + + // Default: handle MID_EXIT + if (menu.getId() == TMenu.MID_EXIT) { + /* + TODO + if (messageBox("Confirmation", "Exit application?", + TMessageBox.Type.YESNO).result == TMessageBox.Result.YES) { + quit = true; + } + // System.err.printf("onMenu MID_EXIT result: quit = %s\n", quit); + repaint = true; + return true; + */ + quit = true; + repaint = true; + return true; + } + + /* + TODO + if (menu.id == TMenu.MID_SHELL) { + openTerminal(0, 0, TWindow.Flag.RESIZABLE); + repaint = true; + return true; } - windows ~= window; - window.active = true; - window.z = 0; */ + + 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; + } + return false; } + /** + * Method that TApplication subclasses can override to handle keystrokes. + * + * @param keypress keystroke event + * @return if true, this event was consumed + */ + protected boolean onKeypress(final TKeypressEvent keypress) { + // Default: only menu shortcuts + + // Process Alt-F, Alt-E, etc. menu shortcut keys + if (!keypress.getKey().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; + } + } + } + + return false; + } + + /** + * Add a keyboard accelerator to the global hash. + * + * @param item menu item this accelerator relates to + * @param keypress keypress that will dispatch a TMenuEvent + */ + public final void addAccelerator(final TMenuItem item, + final TKeypress keypress) { + /* + TODO + assert((keypress in accelerators) is null); + accelerators[keypress] = item; + */ + } + + /** + * Recompute menu x positions based on their title length. + */ + public final void recomputeMenuX() { + int x = 0; + for (TMenu menu: menus) { + menu.setX(x); + x += menu.getTitle().length() + 2; + } + } + + /** + * Post an event to process and turn off the menu. + * + * @param event new event to add to the queue + */ + public final void addMenuEvent(final TInputEvent event) { + synchronized (fillEventQueue) { + fillEventQueue.add(event); + } + closeMenu(); + } + + /** + * Add a sub-menu to the list of open sub-menus. + * + * @param menu sub-menu + */ + public final void addSubMenu(final TMenu menu) { + subMenus.add(menu); + } + + /** + * Convenience function to add a top-level menu. + * + * @param title menu title + * @return the new menu + */ + public final TMenu addMenu(String title) { + int x = 0; + int y = 0; + TMenu menu = new TMenu(this, x, y, title); + menus.add(menu); + recomputeMenuX(); + return menu; + } + + /** + * Convenience function to add a default "File" menu. + * + * @return the new menu + */ + public final TMenu addFileMenu() { + TMenu fileMenu = addMenu("&File"); + fileMenu.addDefaultItem(TMenu.MID_OPEN_FILE); + fileMenu.addSeparator(); + fileMenu.addDefaultItem(TMenu.MID_SHELL); + fileMenu.addDefaultItem(TMenu.MID_EXIT); + return fileMenu; + } + + /** + * Convenience function to add a default "Edit" menu. + * + * @return the new menu + */ + public final TMenu addEditMenu() { + TMenu editMenu = addMenu("&Edit"); + editMenu.addDefaultItem(TMenu.MID_CUT); + editMenu.addDefaultItem(TMenu.MID_COPY); + editMenu.addDefaultItem(TMenu.MID_PASTE); + editMenu.addDefaultItem(TMenu.MID_CLEAR); + return editMenu; + } + + /** + * Convenience function to add a default "Window" menu. + * + * @return the new menu + */ + final public TMenu addWindowMenu() { + TMenu windowMenu = addMenu("&Window"); + windowMenu.addDefaultItem(TMenu.MID_TILE); + windowMenu.addDefaultItem(TMenu.MID_CASCADE); + windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL); + windowMenu.addSeparator(); + windowMenu.addDefaultItem(TMenu.MID_WINDOW_MOVE); + windowMenu.addDefaultItem(TMenu.MID_WINDOW_ZOOM); + windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT); + windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS); + windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE); + return windowMenu; + } + + /** + * Close all open windows. + */ + private void closeAllWindows() { + // Don't do anything if we are in the menu + if (activeMenu != null) { + return; + } + for (TWindow window: windows) { + closeWindow(window); + } + } + + /** + * 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() { + // 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)); + // System.err.printf("Z %s a %s b %s c %s newWidth %s newHeight1 %s newHeight2 %s", + // z, a, b, c, newWidth, newHeight1, newHeight2); + + 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() { + // 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 w: sorted) { + w.setX(x); + w.setY(y); + x++; + y++; + if (x > getScreen().getWidth()) { + x = 0; + } + if (y >= getScreen().getHeight()) { + y = 1; + } + } + } }