X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTApplication.java;h=2537c3e4b5bbdfddb0d05837000bd6effea21a48;hb=2b4274048c2f409b5eba8373ab3018aa75911c73;hp=34f1559007090b31eef65a4d79be9288ed54dec7;hpb=5255f69c14d2c19f0e2f1f4ae8d9a106c92d3b8c;p=fanfix.git diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 34f1559..2537c3e 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * Copyright (C) 2019 Kevin Lamonte * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -36,15 +36,16 @@ import java.io.PrintWriter; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.ResourceBundle; +import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.bits.ColorTheme; import jexer.event.TCommandEvent; @@ -54,13 +55,14 @@ import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; import jexer.backend.Backend; -import jexer.backend.Screen; import jexer.backend.MultiBackend; +import jexer.backend.Screen; import jexer.backend.SwingBackend; import jexer.backend.ECMA48Backend; import jexer.backend.TWindowBackend; import jexer.menu.TMenu; import jexer.menu.TMenuItem; +import jexer.menu.TSubMenu; import static jexer.TCommand.*; import static jexer.TKeypress.*; @@ -160,6 +162,21 @@ public class TApplication implements Runnable { */ private int oldMouseY; + /** + * Old drawn version of mouse coordinate X. + */ + private int oldDrawnMouseX; + + /** + * Old drawn version mouse coordinate Y. + */ + private int oldDrawnMouseY; + + /** + * Old drawn version mouse cell. + */ + private Cell oldDrawnMouseCell = new Cell(); + /** * The last mouse up click time, used to determine if this is a mouse * double-click. @@ -266,6 +283,16 @@ public class TApplication implements Runnable { */ private boolean focusFollowsMouse = false; + /** + * The images that might be displayed. Note package private access. + */ + private List images; + + /** + * The list of commands to run before the next I/O check. + */ + private List invokeLaters = new LinkedList(); + /** * WidgetEventHandler is the main event consumer loop. There are at most * two such threads in existence: the primary for normal case and a @@ -300,6 +327,21 @@ public class TApplication implements Runnable { * The consumer loop. */ public void run() { + // Wrap everything in a try, so that if we go belly up we can let + // the user have their terminal back. + try { + runImpl(); + } catch (Throwable t) { + this.application.restoreConsole(); + t.printStackTrace(); + this.application.exit(); + } + } + + /** + * The consumer loop. + */ + private void runImpl() { boolean first = true; // Loop forever @@ -327,9 +369,10 @@ public class TApplication implements Runnable { } if (debugThreads) { - System.err.printf("%d %s %s sleep %d millis\n", + System.err.printf("%d %s %s %s sleep %d millis\n", System.currentTimeMillis(), this, - primary ? "primary" : "secondary", timeout); + primary ? "primary" : "secondary", + Thread.currentThread(), timeout); } synchronized (this) { @@ -337,9 +380,10 @@ public class TApplication implements Runnable { } if (debugThreads) { - System.err.printf("%d %s %s AWAKE\n", + System.err.printf("%d %s %s %s AWAKE\n", System.currentTimeMillis(), this, - primary ? "primary" : "secondary"); + primary ? "primary" : "secondary", + Thread.currentThread()); } if ((!primary) @@ -385,15 +429,16 @@ public class TApplication implements Runnable { ) { // Secondary thread, time to exit. - // DO NOT UNLOCK. Primary thread just came back from - // primaryHandleEvent() and will unlock in the else - // block below. Just wake it up. + // Eliminate my reference so that wakeEventHandler() + // resumes working on the primary. + application.secondaryEventHandler = null; + + // We are ready to exit, wake up the primary thread. + // Remember that it is currently sleeping inside its + // primaryHandleEvent(). synchronized (application.primaryEventHandler) { application.primaryEventHandler.notify(); } - // Now eliminate my reference so that - // wakeEventHandler() resumes working on the primary. - application.secondaryEventHandler = null; // All done! return; @@ -552,15 +597,16 @@ public class TApplication implements Runnable { private void TApplicationImpl() { theme = new ColorTheme(); desktopBottom = getScreen().getHeight() - 1; - fillEventQueue = new ArrayList(); - drainEventQueue = new ArrayList(); + fillEventQueue = new LinkedList(); + drainEventQueue = new LinkedList(); windows = new LinkedList(); - menus = new LinkedList(); - subMenus = new LinkedList(); + menus = new ArrayList(); + subMenus = new ArrayList(); timers = new LinkedList(); accelerators = new HashMap(); - menuItems = new ArrayList(); + menuItems = new LinkedList(); desktop = new TDesktop(this); + images = new LinkedList(); // Special case: the Swing backend needs to have a timer to drive its // blink state. @@ -593,6 +639,8 @@ public class TApplication implements Runnable { * Run this application until it exits. */ public void run() { + // System.err.println("*** TApplication.run() begins ***"); + // Start the main consumer thread primaryEventHandler = new WidgetEventHandler(this, true); (new Thread(primaryEventHandler)).start(); @@ -617,14 +665,14 @@ public class TApplication implements Runnable { try { if (debugThreads) { System.err.println(System.currentTimeMillis() + - " MAIN sleep"); + " " + Thread.currentThread() + " MAIN sleep"); } this.wait(); if (debugThreads) { System.err.println(System.currentTimeMillis() + - " MAIN AWAKE"); + " " + Thread.currentThread() + " MAIN AWAKE"); } } catch (InterruptedException e) { // I'm awake and don't care why, let's see what's @@ -676,6 +724,11 @@ public class TApplication implements Runnable { // resources. closeAllWindows(); + // Give the overarching application an opportunity to release + // resources. + onExit(); + + // System.err.println("*** TApplication.run() exits ***"); } // ------------------------------------------------------------------------ @@ -694,7 +747,8 @@ public class TApplication implements Runnable { if (command.equals(cmExit)) { if (messageBox(i18n.getString("exitDialogTitle"), i18n.getString("exitDialogText"), - TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { + TMessageBox.Type.YESNO).isYes()) { + exit(); } return true; @@ -744,7 +798,8 @@ public class TApplication implements Runnable { if (menu.getId() == TMenu.MID_EXIT) { if (messageBox(i18n.getString("exitDialogTitle"), i18n.getString("exitDialogText"), - TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { + TMessageBox.Type.YESNO).isYes()) { + exit(); } return true; @@ -772,9 +827,18 @@ public class TApplication implements Runnable { return true; } if (menu.getId() == TMenu.MID_REPAINT) { + getScreen().clearPhysical(); doRepaint(); return true; } + if (menu.getId() == TMenu.MID_VIEW_IMAGE) { + openImage(); + return true; + } + if (menu.getId() == TMenu.MID_CHANGE_FONT) { + new TFontChooserWindow(this); + return true; + } return false; } @@ -858,7 +922,7 @@ public class TApplication implements Runnable { // Abort everything if (event instanceof TCommandEvent) { TCommandEvent command = (TCommandEvent) event; - if (command.getCmd().equals(cmAbort)) { + if (command.equals(cmAbort)) { exit(); return; } @@ -880,6 +944,7 @@ public class TApplication implements Runnable { if (desktop != null) { desktop.setDimensions(0, 0, resize.getWidth(), resize.getHeight() - 1); + desktop.onResize(resize); } // Change menu edges if needed. @@ -911,7 +976,8 @@ public class TApplication implements Runnable { private void primaryHandleEvent(final TInputEvent event) { if (debugEvents) { - System.err.printf("Handle event: %s\n", event); + System.err.printf("%s primaryHandleEvent: %s\n", + Thread.currentThread(), event); } TMouseEvent doubleClick = null; @@ -926,7 +992,10 @@ public class TApplication implements Runnable { mouseX = mouse.getX(); mouseY = mouse.getY(); } else { - if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) { + if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) + && (!mouse.isMouseWheelUp()) + && (!mouse.isMouseWheelDown()) + ) { if ((mouse.getTime().getTime() - lastMouseUpTime) < doubleClickTime) { @@ -1094,6 +1163,11 @@ public class TApplication implements Runnable { private void secondaryHandleEvent(final TInputEvent event) { TMouseEvent doubleClick = null; + if (debugEvents) { + System.err.printf("%s secondaryHandleEvent: %s\n", + Thread.currentThread(), event); + } + // Peek at the mouse position if (event instanceof TMouseEvent) { TMouseEvent mouse = (TMouseEvent) event; @@ -1103,7 +1177,10 @@ public class TApplication implements Runnable { mouseX = mouse.getX(); mouseY = mouse.getY(); } else { - if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) { + if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) + && (!mouse.isMouseWheelUp()) + && (!mouse.isMouseWheelDown()) + ) { if ((mouse.getTime().getTime() - lastMouseUpTime) < doubleClickTime) { @@ -1161,6 +1238,11 @@ public class TApplication implements Runnable { * Yield to the secondary thread. */ public final void yield() { + if (debugThreads) { + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " yield()\n"); + } + assert (secondaryEventReceiver != null); while (secondaryEventReceiver != null) { @@ -1205,7 +1287,8 @@ public class TApplication implements Runnable { keepTimers.add(timer); } } - timers = keepTimers; + timers.clear(); + timers.addAll(keepTimers); } // Call onIdle's @@ -1215,6 +1298,15 @@ public class TApplication implements Runnable { if (desktop != null) { desktop.onIdle(); } + + // Run any invokeLaters + synchronized (invokeLaters) { + for (Runnable invoke: invokeLaters) { + invoke.run(); + } + invokeLaters.clear(); + } + } /** @@ -1241,6 +1333,32 @@ public class TApplication implements Runnable { // TApplication ----------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Place a command on the run queue, and run it before the next round of + * checking I/O. + * + * @param command the command to run later + */ + public void invokeLater(final Runnable command) { + synchronized (invokeLaters) { + invokeLaters.add(command); + } + doRepaint(); + } + + /** + * Restore the console to sane defaults. This is meant to be used for + * improper exits (e.g. a caught exception in main()), and should not be + * necessary for normal program termination. + */ + public void restoreConsole() { + if (backend != null) { + if (backend instanceof ECMA48Backend) { + backend.shutdown(); + } + } + } + /** * Get the Backend. * @@ -1339,7 +1457,7 @@ public class TApplication implements Runnable { * @return a copy of the list of windows for this application */ public final List getAllWindows() { - List result = new LinkedList(); + List result = new ArrayList(); result.addAll(windows); return result; } @@ -1368,12 +1486,49 @@ public class TApplication implements Runnable { * Display the about dialog. */ protected void showAboutDialog() { + String version = getClass().getPackage().getImplementationVersion(); + if (version == null) { + // This is Java 9+, use a hardcoded string here. + version = "0.3.1"; + } messageBox(i18n.getString("aboutDialogTitle"), - MessageFormat.format(i18n.getString("aboutDialogText"), - this.getClass().getPackage().getImplementationVersion()), + MessageFormat.format(i18n.getString("aboutDialogText"), version), TMessageBox.Type.OK); } + /** + * Handle the Tool | Open image menu item. + */ + private void openImage() { + try { + List filters = new ArrayList(); + filters.add("^.*\\.[Jj][Pp][Gg]$"); + filters.add("^.*\\.[Jj][Pp][Ee][Gg]$"); + filters.add("^.*\\.[Pp][Nn][Gg]$"); + filters.add("^.*\\.[Gg][Ii][Ff]$"); + filters.add("^.*\\.[Bb][Mm][Pp]$"); + String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN, filters); + if (filename != null) { + new TImageWindow(this, new File(filename)); + } + } catch (IOException e) { + // Show this exception to the user. + new TExceptionDialog(this, e); + } + } + + /** + * Check if application is still running. + * + * @return true if the application is running + */ + public final boolean isRunning() { + if (quit == true) { + return false; + } + return true; + } + // ------------------------------------------------------------------------ // Screen refresh loop ---------------------------------------------------- // ------------------------------------------------------------------------ @@ -1388,19 +1543,42 @@ public class TApplication implements Runnable { if (debugThreads) { System.err.printf("%d %s invertCell() %d %d\n", System.currentTimeMillis(), Thread.currentThread(), x, y); + + if (activeWindow != null) { + System.err.println("activeWindow.hasHiddenMouse() " + + activeWindow.hasHiddenMouse()); + } } - CellAttributes attr = getScreen().getAttrXY(x, y); - if (attr.getForeColorRGB() < 0) { - attr.setForeColor(attr.getForeColor().invert()); - } else { - attr.setForeColorRGB(attr.getForeColorRGB() ^ 0x00ffffff); + + // If this cell is on top of a visible window that has requested a + // hidden mouse, bail out. + if ((activeWindow != null) && (activeMenu == null)) { + if ((activeWindow.hasHiddenMouse() == true) + && (x > activeWindow.getX()) + && (x < activeWindow.getX() + activeWindow.getWidth() - 1) + && (y > activeWindow.getY()) + && (y < activeWindow.getY() + activeWindow.getHeight() - 1) + ) { + return; + } } - if (attr.getBackColorRGB() < 0) { - attr.setBackColor(attr.getBackColor().invert()); + + Cell cell = getScreen().getCharXY(x, y); + if (cell.isImage()) { + cell.invertImage(); } else { - attr.setBackColorRGB(attr.getBackColorRGB() ^ 0x00ffffff); + if (cell.getForeColorRGB() < 0) { + cell.setForeColor(cell.getForeColor().invert()); + } else { + cell.setForeColorRGB(cell.getForeColorRGB() ^ 0x00ffffff); + } + if (cell.getBackColorRGB() < 0) { + cell.setBackColor(cell.getBackColor().invert()); + } else { + cell.setBackColorRGB(cell.getBackColorRGB() ^ 0x00ffffff); + } } - getScreen().putAttrXY(x, y, attr, false); + getScreen().putCharXY(x, y, cell); } /** @@ -1414,25 +1592,62 @@ public class TApplication implements Runnable { System.currentTimeMillis(), Thread.currentThread()); } + // I don't think this does anything useful anymore... if (!repaint) { if (debugThreads) { System.err.printf("%d %s drawAll() !repaint\n", System.currentTimeMillis(), Thread.currentThread()); } - 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 ((oldDrawnMouseX != mouseX) || (oldDrawnMouseY != mouseY)) { + if (debugThreads) { + System.err.printf("%d %s drawAll() !repaint MOUSE\n", + System.currentTimeMillis(), Thread.currentThread()); } - if (getScreen().isDirty()) { - backend.flushScreen(); + + // The only thing that has happened is the mouse moved. + + // Redraw the old cell at that position, and save the cell at + // the new mouse position. + if (debugThreads) { + System.err.printf("%d %s restoreImage() %d %d\n", + System.currentTimeMillis(), Thread.currentThread(), + oldDrawnMouseX, oldDrawnMouseY); } - return; + oldDrawnMouseCell.restoreImage(); + getScreen().putCharXY(oldDrawnMouseX, oldDrawnMouseY, + oldDrawnMouseCell); + oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY); + if ((images.size() > 0) && (backend instanceof ECMA48Backend)) { + // Special case: the entire row containing the mouse has + // to be re-drawn if it has any image data, AND any rows + // in between. + if (oldDrawnMouseY != mouseY) { + for (int i = oldDrawnMouseY; ;) { + getScreen().unsetImageRow(i); + if (i == mouseY) { + break; + } + if (oldDrawnMouseY < mouseY) { + i++; + } else { + i--; + } + } + } else { + getScreen().unsetImageRow(mouseY); + } + } + + // Draw mouse at the new position. + invertCell(mouseX, mouseY); + + oldDrawnMouseX = mouseX; + oldDrawnMouseY = mouseY; } + if ((images.size() > 0) || getScreen().isDirty()) { + backend.flushScreen(); + } + return; } if (debugThreads) { @@ -1452,7 +1667,7 @@ public class TApplication implements Runnable { } // Draw each window in reverse Z order - List sorted = new LinkedList(windows); + List sorted = new ArrayList(windows); Collections.sort(sorted); TWindow topLevel = null; if (sorted.size() > 0) { @@ -1493,7 +1708,7 @@ public class TApplication implements Runnable { 0, menu.getMnemonic().getShortcut(), menuMnemonicColor); if (menu.isActive()) { - menu.drawChildren(); + ((TWindow) menu).drawChildren(); // Reset the screen clipping so we can draw the next title. getScreen().resetClipping(); } @@ -1503,8 +1718,9 @@ public class TApplication implements Runnable { for (TMenu menu: subMenus) { // Reset the screen clipping so we can draw the next sub-menu. getScreen().resetClipping(); - menu.drawChildren(); + ((TWindow) menu).drawChildren(); } + getScreen().resetClipping(); // Draw the status bar of the top-level window TStatusBar statusBar = null; @@ -1524,9 +1740,34 @@ public class TApplication implements Runnable { } // Draw the mouse pointer + if (debugThreads) { + System.err.printf("%d %s restoreImage() %d %d\n", + System.currentTimeMillis(), Thread.currentThread(), + oldDrawnMouseX, oldDrawnMouseY); + } + oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY); + if ((images.size() > 0) && (backend instanceof ECMA48Backend)) { + // Special case: the entire row containing the mouse has to be + // re-drawn if it has any image data, AND any rows in between. + if (oldDrawnMouseY != mouseY) { + for (int i = oldDrawnMouseY; ;) { + getScreen().unsetImageRow(i); + if (i == mouseY) { + break; + } + if (oldDrawnMouseY < mouseY) { + i++; + } else { + i--; + } + } + } else { + getScreen().unsetImageRow(mouseY); + } + } invertCell(mouseX, mouseY); - oldMouseX = mouseX; - oldMouseY = mouseY; + oldDrawnMouseX = mouseX; + oldDrawnMouseY = mouseY; // Place the cursor if it is visible if (!menuIsActive) { @@ -1542,9 +1783,8 @@ public class TApplication implements Runnable { activeWidget.getCursorAbsoluteY()); cursor = true; } else { - getScreen().putCursor(false, - activeWidget.getCursorAbsoluteX(), - activeWidget.getCursorAbsoluteY()); + // Turn off the cursor. Also place it at 0,0. + getScreen().putCursor(false, 0, 0); cursor = false; } } @@ -1557,7 +1797,11 @@ public class TApplication implements Runnable { } // Flush the screen contents - if (getScreen().isDirty()) { + if ((images.size() > 0) || getScreen().isDirty()) { + if (debugThreads) { + System.err.printf("%d %s backend.flushScreen()\n", + System.currentTimeMillis(), Thread.currentThread()); + } backend.flushScreen(); } @@ -1574,6 +1818,14 @@ public class TApplication implements Runnable { } } + /** + * Subclasses can use this hook to cleanup resources. Called as the last + * step of TApplication.run(). + */ + public void onExit() { + // Default does nothing. + } + // ------------------------------------------------------------------------ // TWindow management ----------------------------------------------------- // ------------------------------------------------------------------------ @@ -1689,10 +1941,20 @@ public class TApplication implements Runnable { assert (!window.isActive()); if (activeWindow != null) { - assert (activeWindow.getZ() == 0); + // TODO: see if this assertion is really necessary. + // assert (activeWindow.getZ() == 0); activeWindow.setActive(false); - activeWindow.setZ(window.getZ()); + + // Increment every window Z that is on top of window + for (TWindow w: windows) { + if (w == window) { + continue; + } + if (w.getZ() < window.getZ()) { + w.setZ(w.getZ() + 1); + } + } // Unset activeWindow now before unfocus, so that a window // lifecycle change inside onUnfocus() doesn't call @@ -1791,6 +2053,10 @@ public class TApplication implements Runnable { return; } + // Let window know that it is about to be closed, while it is still + // visible on screen. + window.onPreClose(); + synchronized (windows) { // Whatever window might be moving/dragging, stop it now. for (TWindow w: windows) { @@ -1802,29 +2068,33 @@ public class TApplication implements Runnable { int z = window.getZ(); window.setZ(-1); window.onUnfocus(); + windows.remove(window); Collections.sort(windows); - windows.remove(0); activeWindow = null; + int newZ = 0; + boolean foundNextWindow = false; + for (TWindow w: windows) { + w.setZ(newZ); + newZ++; // Do not activate a hidden window. if (w.isHidden()) { continue; } - 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(); - } - } + if (foundNextWindow == false) { + foundNextWindow = true; + w.setActive(true); + w.onFocus(); + assert (activeWindow == null); + activeWindow = w; + continue; + } + + if (w.isActive()) { + w.setActive(false); + w.onUnfocus(); } } } @@ -2005,6 +2275,21 @@ public class TApplication implements Runnable { return false; } + /** + * Check if there is a window with overridden menu flag on top. + * + * @return true if the active window is overriding the menu + */ + private boolean overrideMenuWindowActive() { + if (activeWindow != null) { + if (activeWindow.hasOverriddenMenu()) { + return true; + } + } + + return false; + } + /** * Close all open windows. */ @@ -2050,7 +2335,7 @@ public class TApplication implements Runnable { int newHeight1 = ((getScreen().getHeight() - 1) / b); int newHeight2 = ((getScreen().getHeight() - 1) / (b + c)); - List sorted = new LinkedList(windows); + List sorted = new ArrayList(windows); Collections.sort(sorted); Collections.reverse(sorted); for (int i = 0; i < sorted.size(); i++) { @@ -2062,6 +2347,9 @@ public class TApplication implements Runnable { } TWindow w = sorted.get(i); + int oldWidth = w.getWidth(); + int oldHeight = w.getHeight(); + w.setX(logicalX * newWidth); w.setWidth(newWidth); if (i >= ((a - 1) * b)) { @@ -2071,6 +2359,12 @@ public class TApplication implements Runnable { w.setY((logicalY * newHeight1) + 1); w.setHeight(newHeight1); } + if ((w.getWidth() != oldWidth) + || (w.getHeight() != oldHeight) + ) { + w.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, + w.getWidth(), w.getHeight())); + } } } } @@ -2086,7 +2380,7 @@ public class TApplication implements Runnable { } int x = 0; int y = 1; - List sorted = new LinkedList(windows); + List sorted = new ArrayList(windows); Collections.sort(sorted); Collections.reverse(sorted); for (TWindow window: sorted) { @@ -2235,6 +2529,46 @@ public class TApplication implements Runnable { window.setY(windowY); } + // ------------------------------------------------------------------------ + // TImage management ------------------------------------------------------ + // ------------------------------------------------------------------------ + + /** + * Add an image to the list. Note package private access. + * + * @param image the image to add + * @throws IllegalArgumentException if the image is already used in + * another TApplication + */ + final void addImage(final TImage image) { + if ((image.getApplication() != null) + && (image.getApplication() != this) + ) { + throw new IllegalArgumentException("Image " + image + + " is already " + "part of application " + + image.getApplication()); + } + images.add(image); + } + + /** + * Remove an image from the list. Note package private access. + * + * @param image the image to remove + * @throws IllegalArgumentException if the image is already used in + * another TApplication + */ + final void removeImage(final TImage image) { + if ((image.getApplication() != null) + && (image.getApplication() != this) + ) { + throw new IllegalArgumentException("Image " + image + + " is already " + "part of application " + + image.getApplication()); + } + images.remove(image); + } + // ------------------------------------------------------------------------ // TMenu management ------------------------------------------------------- // ------------------------------------------------------------------------ @@ -2249,7 +2583,7 @@ public class TApplication implements Runnable { */ private boolean mouseOnMenu(final TMouseEvent mouse) { assert (activeMenu != null); - List menus = new LinkedList(subMenus); + List menus = new ArrayList(subMenus); Collections.reverse(menus); for (TMenu menu: menus) { if (menu.mouseWouldHit(mouse)) { @@ -2286,6 +2620,7 @@ public class TApplication implements Runnable { if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) && (mouse.isMouse1()) && (!modalWindowActive()) + && (!overrideMenuWindowActive()) && (mouse.getAbsoluteY() == 0) ) { @@ -2351,7 +2686,7 @@ public class TApplication implements Runnable { if (((focusFollowsMouse == true) && (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)) - || (mouse.getType() == TMouseEvent.Type.MOUSE_UP) + || (mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) ) { synchronized (windows) { Collections.sort(windows); @@ -2379,9 +2714,11 @@ public class TApplication implements Runnable { assert (windows.get(0).isActive()); assert (windows.get(0) == activeWindow); assert (!window.isActive()); - activeWindow.onUnfocus(); - activeWindow.setActive(false); - activeWindow.setZ(window.getZ()); + if (activeWindow != null) { + activeWindow.onUnfocus(); + activeWindow.setActive(false); + activeWindow.setZ(window.getZ()); + } activeWindow = window; window.setZ(0); window.setActive(true); @@ -2420,7 +2757,7 @@ public class TApplication implements Runnable { * @return a copy of the menu list */ public final List getAllMenus() { - return new LinkedList(menus); + return new ArrayList(menus); } /** @@ -2491,10 +2828,14 @@ public class TApplication implements Runnable { if (forward) { if (i < menus.size() - 1) { i++; + } else { + i = 0; } } else { if (i > 0) { i--; + } else { + i = menus.size() - 1; } } activeMenu.setActive(false); @@ -2547,6 +2888,7 @@ public class TApplication implements Runnable { for (TMenuItem item: menuItems) { if ((item.getId() >= lower) && (item.getId() <= upper)) { item.setEnabled(false); + item.getParent().activate(0); } } } @@ -2560,6 +2902,7 @@ public class TApplication implements Runnable { for (TMenuItem item: menuItems) { if (item.getId() == id) { item.setEnabled(true); + item.getParent().activate(0); } } } @@ -2575,10 +2918,26 @@ public class TApplication implements Runnable { for (TMenuItem item: menuItems) { if ((item.getId() >= lower) && (item.getId() <= upper)) { item.setEnabled(true); + item.getParent().activate(0); } } } + /** + * Get the menu item associated with this ID. + * + * @param id the menu item ID + * @return the menu item, or null if not found + */ + public final TMenuItem getMenuItem(final int id) { + for (TMenuItem item: menuItems) { + if (item.getId() == id) { + return item; + } + } + return null; + } + /** * Recompute menu x positions based on their title length. */ @@ -2658,6 +3017,22 @@ public class TApplication implements Runnable { return menu; } + /** + * Convenience function to add a default tools (hamburger) menu. + * + * @return the new menu + */ + public final TMenu addToolMenu() { + TMenu toolMenu = addMenu(i18n.getString("toolMenuTitle")); + toolMenu.addDefaultItem(TMenu.MID_REPAINT); + toolMenu.addDefaultItem(TMenu.MID_VIEW_IMAGE); + toolMenu.addDefaultItem(TMenu.MID_CHANGE_FONT); + TStatusBar toolStatusBar = toolMenu.newStatusBar(i18n. + getString("toolMenuStatus")); + toolStatusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help")); + return toolMenu; + } + /** * Convenience function to add a default "File" menu. * @@ -2665,9 +3040,8 @@ public class TApplication implements Runnable { */ public final TMenu addFileMenu() { TMenu fileMenu = addMenu(i18n.getString("fileMenuTitle")); - fileMenu.addDefaultItem(TMenu.MID_OPEN_FILE); - fileMenu.addSeparator(); fileMenu.addDefaultItem(TMenu.MID_SHELL); + fileMenu.addSeparator(); fileMenu.addDefaultItem(TMenu.MID_EXIT); TStatusBar statusBar = fileMenu.newStatusBar(i18n. getString("fileMenuStatus")); @@ -2735,6 +3109,63 @@ public class TApplication implements Runnable { return helpMenu; } + /** + * Convenience function to add a default "Table" menu. + * + * @return the new menu + */ + public final TMenu addTableMenu() { + TMenu tableMenu = addMenu(i18n.getString("tableMenuTitle")); + tableMenu.addDefaultItem(TMenu.MID_TABLE_RENAME_COLUMN, false); + tableMenu.addDefaultItem(TMenu.MID_TABLE_RENAME_ROW, false); + tableMenu.addSeparator(); + + TSubMenu viewMenu = tableMenu.addSubMenu(i18n. + getString("tableSubMenuView")); + viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_ROW_LABELS, false); + viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_COLUMN_LABELS, false); + viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW, false); + viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN, false); + + TSubMenu borderMenu = tableMenu.addSubMenu(i18n. + getString("tableSubMenuBorders")); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_NONE, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_ALL, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_CELL_NONE, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_CELL_ALL, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_RIGHT, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_LEFT, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_TOP, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_BOTTOM, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_DOUBLE_BOTTOM, false); + borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_THICK_BOTTOM, false); + TSubMenu deleteMenu = tableMenu.addSubMenu(i18n. + getString("tableSubMenuDelete")); + deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_LEFT, false); + deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_UP, false); + deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_ROW, false); + deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_COLUMN, false); + TSubMenu insertMenu = tableMenu.addSubMenu(i18n. + getString("tableSubMenuInsert")); + insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_LEFT, false); + insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_RIGHT, false); + insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_ABOVE, false); + insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_BELOW, false); + TSubMenu columnMenu = tableMenu.addSubMenu(i18n. + getString("tableSubMenuColumn")); + columnMenu.addDefaultItem(TMenu.MID_TABLE_COLUMN_NARROW, false); + columnMenu.addDefaultItem(TMenu.MID_TABLE_COLUMN_WIDEN, false); + TSubMenu fileMenu = tableMenu.addSubMenu(i18n. + getString("tableSubMenuFile")); + fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_SAVE_CSV, false); + fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_SAVE_TEXT, false); + + TStatusBar statusBar = tableMenu.newStatusBar(i18n. + getString("tableMenuStatus")); + statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help")); + return tableMenu; + } + // ------------------------------------------------------------------------ // TTimer management ------------------------------------------------------ // ------------------------------------------------------------------------ @@ -2860,6 +3291,22 @@ public class TApplication implements Runnable { return new TInputBox(this, title, caption, text); } + /** + * Convenience function to spawn an input box. + * + * @param title window title, will be centered along the top border + * @param caption message to display. Use embedded newlines to get a + * multi-line box. + * @param text initial text to seed the field with + * @param type one of the Type constants. Default is Type.OK. + * @return the new input box + */ + public final TInputBox inputBox(final String title, final String caption, + final String text, final TInputBox.Type type) { + + return new TInputBox(this, title, caption, text, type); + } + /** * Convenience function to open a terminal window. * @@ -2871,6 +3318,20 @@ public class TApplication implements Runnable { return openTerminal(x, y, TWindow.RESIZABLE); } + /** + * Convenience function to open a terminal window. + * + * @param x column relative to parent + * @param y row relative to parent + * @param closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final boolean closeOnExit) { + + return openTerminal(x, y, TWindow.RESIZABLE, closeOnExit); + } + /** * Convenience function to open a terminal window. * @@ -2885,6 +3346,21 @@ public class TApplication implements Runnable { return new TTerminalWindow(this, x, y, flags); } + /** + * Convenience function to open a terminal window. + * + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final int flags, final boolean closeOnExit) { + + return new TTerminalWindow(this, x, y, flags, closeOnExit); + } + /** * Convenience function to open a terminal window and execute a custom * command line inside it. @@ -2900,6 +3376,22 @@ public class TApplication implements Runnable { return openTerminal(x, y, TWindow.RESIZABLE, commandLine); } + /** + * Convenience function to open a terminal window and execute a custom + * command line inside it. + * + * @param x column relative to parent + * @param y row relative to parent + * @param commandLine the command line to execute + * @param closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final String commandLine, final boolean closeOnExit) { + + return openTerminal(x, y, TWindow.RESIZABLE, commandLine, closeOnExit); + } + /** * Convenience function to open a terminal window and execute a custom * command line inside it. @@ -2916,6 +3408,23 @@ public class TApplication implements Runnable { return new TTerminalWindow(this, x, y, flags, command); } + /** + * Convenience function to open a terminal window and execute a custom + * command line inside it. + * + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param command the command line to execute + * @param closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final int flags, final String [] command, final boolean closeOnExit) { + + return new TTerminalWindow(this, x, y, flags, command, closeOnExit); + } + /** * Convenience function to open a terminal window and execute a custom * command line inside it. @@ -2929,7 +3438,25 @@ public class TApplication implements Runnable { public final TTerminalWindow openTerminal(final int x, final int y, final int flags, final String commandLine) { - return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s")); + return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s+")); + } + + /** + * Convenience function to open a terminal window and execute a custom + * command line inside it. + * + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param commandLine the command line to execute + * @param closeOnExit if true, close the window when the command exits + * @return the terminal new window + */ + public final TTerminalWindow openTerminal(final int x, final int y, + final int flags, final String commandLine, final boolean closeOnExit) { + + return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s+"), + closeOnExit); } /** @@ -2960,6 +3487,42 @@ public class TApplication implements Runnable { return box.getFilename(); } + /** + * Convenience function to spawn a file open box. + * + * @param path path of selected file + * @param type one of the Type constants + * @param filter a string that files must match to be displayed + * @return the result of the new file open box + * @throws IOException of a java.io operation throws + */ + public final String fileOpenBox(final String path, + final TFileOpenBox.Type type, final String filter) throws IOException { + + ArrayList filters = new ArrayList(); + filters.add(filter); + + TFileOpenBox box = new TFileOpenBox(this, path, type, filters); + return box.getFilename(); + } + + /** + * Convenience function to spawn a file open box. + * + * @param path path of selected file + * @param type one of the Type constants + * @param filters a list of strings that files must match to be displayed + * @return the result of the new file open box + * @throws IOException of a java.io operation throws + */ + public final String fileOpenBox(final String path, + final TFileOpenBox.Type type, + final List filters) throws IOException { + + TFileOpenBox box = new TFileOpenBox(this, path, type, filters); + return box.getFilename(); + } + /** * Convenience function to create a new window and make it active. * Window will be located at (0, 0). @@ -3029,18 +3592,4 @@ public class TApplication implements Runnable { return window; } - /** - * Convenience function to open a file in an editor window and make it - * active. - * - * @param file the file to open - * @return the new editor window - * @throws IOException if a java.io operation throws - */ - public final TEditorWindow addEditor(final File file) throws IOException { - - TEditorWindow editor = new TEditorWindow(this, file); - return editor; - } - }