X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTApplication.java;h=b21c066fefb8ecd11dda667aec245e0ea28c98db;hb=a75902faaade579e3ee7fe68b0e18e49148410de;hp=20406b1f8e82aa654efa86081c085d5f3883af03;hpb=00691e80f2f135f92be739e2b7e86775a2357276;p=fanfix.git diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 20406b1..b21c066 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -48,6 +48,7 @@ import java.util.ResourceBundle; import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.bits.ColorTheme; +import jexer.bits.StringUtils; import jexer.event.TCommandEvent; import jexer.event.TInputEvent; import jexer.event.TKeypressEvent; @@ -62,6 +63,7 @@ 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.*; @@ -131,6 +133,11 @@ public class TApplication implements Runnable { */ private volatile WidgetEventHandler secondaryEventHandler; + /** + * The screen handler thread. + */ + private volatile ScreenHandler screenHandler; + /** * The widget receiving events from the secondary event handler thread. */ @@ -283,14 +290,14 @@ public class TApplication implements Runnable { private boolean focusFollowsMouse = false; /** - * The images that might be displayed. Note package private access. + * The list of commands to run before the next I/O check. */ - private List images; + private List invokeLaters = new LinkedList(); /** - * The list of commands to run before the next I/O check. + * The last time the screen was resized. */ - private List invokeLaters = new LinkedList(); + private long screenResizeTime = 0; /** * WidgetEventHandler is the main event consumer loop. There are at most @@ -432,9 +439,9 @@ public class TApplication implements Runnable { // resumes working on the primary. application.secondaryEventHandler = null; - // DO NOT UNLOCK. Primary thread just came back from - // primaryHandleEvent() and will unlock in the else - // block below. Just wake it up. + // 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(); } @@ -454,6 +461,94 @@ public class TApplication implements Runnable { } } + /** + * ScreenHandler pushes screen updates to the physical device. + */ + private class ScreenHandler implements Runnable { + /** + * The main application. + */ + private TApplication application; + + /** + * The dirty flag. + */ + private boolean dirty = false; + + /** + * Public constructor. + * + * @param application the main application + */ + public ScreenHandler(final TApplication application) { + this.application = application; + } + + /** + * The screen update 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 update loop. + */ + private void runImpl() { + + // Loop forever + while (!application.quit) { + + // Wait until application notifies me + while (!application.quit) { + try { + synchronized (this) { + if (dirty) { + dirty = false; + break; + } + + // Always check within 50 milliseconds. + this.wait(50); + } + } catch (InterruptedException e) { + // SQUASH + } + } // while (!application.quit) + + // Flush the screen contents + if (debugThreads) { + System.err.printf("%d %s backend.flushScreen()\n", + System.currentTimeMillis(), Thread.currentThread()); + } + synchronized (getScreen()) { + backend.flushScreen(); + } + } // while (true) (main runnable loop) + + // Shutdown the user I/O thread(s) + backend.shutdown(); + } + + /** + * Set the dirty flag. + */ + public void setDirty() { + synchronized (this) { + dirty = true; + } + } + + } + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -605,7 +700,6 @@ public class TApplication implements Runnable { accelerators = new HashMap(); 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. @@ -638,6 +732,12 @@ public class TApplication implements Runnable { * Run this application until it exits. */ public void run() { + // System.err.println("*** TApplication.run() begins ***"); + + // Start the screen updater thread + screenHandler = new ScreenHandler(this); + (new Thread(screenHandler)).start(); + // Start the main consumer thread primaryEventHandler = new WidgetEventHandler(this, true); (new Thread(primaryEventHandler)).start(); @@ -714,13 +814,15 @@ public class TApplication implements Runnable { } } - // Shutdown the user I/O thread(s) - backend.shutdown(); - // Close all the windows. This gives them an opportunity to release // resources. closeAllWindows(); + // Give the overarching application an opportunity to release + // resources. + onExit(); + + // System.err.println("*** TApplication.run() exits ***"); } // ------------------------------------------------------------------------ @@ -827,7 +929,7 @@ public class TApplication implements Runnable { openImage(); return true; } - if (menu.getId() == TMenu.MID_CHANGE_FONT) { + if (menu.getId() == TMenu.MID_SCREEN_OPTIONS) { new TFontChooserWindow(this); return true; } @@ -884,6 +986,9 @@ public class TApplication implements Runnable { drawAll(); } + // Wake up the screen repainter + wakeScreenHandler(); + if (debugThreads) { System.err.printf(System.currentTimeMillis() + " " + Thread.currentThread() + " finishEventProcessing() END\n"); @@ -914,7 +1019,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; } @@ -925,8 +1030,14 @@ public class TApplication implements Runnable { if (event instanceof TResizeEvent) { TResizeEvent resize = (TResizeEvent) event; synchronized (getScreen()) { - getScreen().setDimensions(resize.getWidth(), - resize.getHeight()); + if ((System.currentTimeMillis() - screenResizeTime >= 15) + || (resize.getWidth() < getScreen().getWidth()) + || (resize.getHeight() < getScreen().getHeight()) + ) { + getScreen().setDimensions(resize.getWidth(), + resize.getHeight()); + screenResizeTime = System.currentTimeMillis(); + } desktopBottom = getScreen().getHeight() - 1; mouseX = 0; mouseY = 0; @@ -936,6 +1047,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. @@ -1320,6 +1432,19 @@ public class TApplication implements Runnable { } } + /** + * Wake the sleeping screen handler. + */ + private void wakeScreenHandler() { + if (!started) { + return; + } + + synchronized (screenHandler) { + screenHandler.notify(); + } + } + // ------------------------------------------------------------------------ // TApplication ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -1480,7 +1605,7 @@ public class TApplication implements Runnable { String version = getClass().getPackage().getImplementationVersion(); if (version == null) { // This is Java 9+, use a hardcoded string here. - version = "0.3.0"; + version = "0.3.2"; } messageBox(i18n.getString("aboutDialogTitle"), MessageFormat.format(i18n.getString("aboutDialogText"), version), @@ -1508,6 +1633,18 @@ public class TApplication implements Runnable { } } + /** + * 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 ---------------------------------------------------- // ------------------------------------------------------------------------ @@ -1519,6 +1656,19 @@ public class TApplication implements Runnable { * @param y row position */ private void invertCell(final int x, final int y) { + invertCell(x, y, false); + } + + /** + * Invert the cell color at a position. This is used to track the mouse. + * + * @param x column position + * @param y row position + * @param onlyThisCell if true, only invert this cell + */ + private void invertCell(final int x, final int y, + final boolean onlyThisCell) { + if (debugThreads) { System.err.printf("%d %s invertCell() %d %d\n", System.currentTimeMillis(), Thread.currentThread(), x, y); @@ -1558,6 +1708,29 @@ public class TApplication implements Runnable { } } getScreen().putCharXY(x, y, cell); + if ((onlyThisCell == true) || (cell.getWidth() == Cell.Width.SINGLE)) { + return; + } + + // This cell is one half of a fullwidth glyph. Invert the other + // half. + if (cell.getWidth() == Cell.Width.LEFT) { + if (x < getScreen().getWidth() - 1) { + Cell rightHalf = getScreen().getCharXY(x + 1, y); + if (rightHalf.getWidth() == Cell.Width.RIGHT) { + invertCell(x + 1, y, true); + return; + } + } + } + assert (cell.getWidth() == Cell.Width.RIGHT); + + if (x > 0) { + Cell leftHalf = getScreen().getCharXY(x - 1, y); + if (leftHalf.getWidth() == Cell.Width.LEFT) { + invertCell(x - 1, y, true); + } + } } /** @@ -1596,7 +1769,7 @@ public class TApplication implements Runnable { getScreen().putCharXY(oldDrawnMouseX, oldDrawnMouseY, oldDrawnMouseCell); oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY); - if ((images.size() > 0) && (backend instanceof ECMA48Backend)) { + if (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. @@ -1623,8 +1796,8 @@ public class TApplication implements Runnable { oldDrawnMouseX = mouseX; oldDrawnMouseY = mouseY; } - if ((images.size() > 0) || getScreen().isDirty()) { - backend.flushScreen(); + if (getScreen().isDirty()) { + screenHandler.setDirty(); } return; } @@ -1679,7 +1852,7 @@ public class TApplication implements Runnable { menuMnemonicColor = theme.getColor("tmenu.mnemonic"); } // Draw the menu title - getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ', + getScreen().hLineXY(x, 0, StringUtils.width(menu.getTitle()) + 2, ' ', menuColor); getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor); // Draw the highlight character @@ -1691,7 +1864,7 @@ public class TApplication implements Runnable { // Reset the screen clipping so we can draw the next title. getScreen().resetClipping(); } - x += menu.getTitle().length() + 2; + x += StringUtils.width(menu.getTitle()) + 2; } for (TMenu menu: subMenus) { @@ -1725,7 +1898,7 @@ public class TApplication implements Runnable { oldDrawnMouseX, oldDrawnMouseY); } oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY); - if ((images.size() > 0) && (backend instanceof ECMA48Backend)) { + if (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) { @@ -1775,11 +1948,9 @@ public class TApplication implements Runnable { getScreen().hideCursor(); } - // Flush the screen contents - if ((images.size() > 0) || getScreen().isDirty()) { - backend.flushScreen(); + if (getScreen().isDirty()) { + screenHandler.setDirty(); } - repaint = false; } @@ -1793,6 +1964,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 ----------------------------------------------------- // ------------------------------------------------------------------------ @@ -1908,7 +2087,8 @@ 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); @@ -2241,6 +2421,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. */ @@ -2480,46 +2675,6 @@ 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 ------------------------------------------------------- // ------------------------------------------------------------------------ @@ -2571,6 +2726,7 @@ public class TApplication implements Runnable { if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) && (mouse.isMouse1()) && (!modalWindowActive()) + && (!overrideMenuWindowActive()) && (mouse.getAbsoluteY() == 0) ) { @@ -2583,7 +2739,7 @@ public class TApplication implements Runnable { for (TMenu menu: menus) { if ((mouse.getAbsoluteX() >= menu.getTitleX()) && (mouse.getAbsoluteX() < menu.getTitleX() - + menu.getTitle().length() + 2) + + StringUtils.width(menu.getTitle()) + 2) ) { menu.setActive(true); activeMenu = menu; @@ -2611,7 +2767,7 @@ public class TApplication implements Runnable { for (TMenu menu: menus) { if ((mouse.getAbsoluteX() >= menu.getTitleX()) && (mouse.getAbsoluteX() < menu.getTitleX() - + menu.getTitle().length() + 2) + + StringUtils.width(menu.getTitle()) + 2) ) { menu.setActive(true); activeMenu = menu; @@ -2873,6 +3029,21 @@ public class TApplication implements Runnable { } } + /** + * 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. */ @@ -2881,7 +3052,7 @@ public class TApplication implements Runnable { for (TMenu menu: menus) { menu.setX(x); menu.setTitleX(x); - x += menu.getTitle().length() + 2; + x += StringUtils.width(menu.getTitle()) + 2; // Don't let the menu window exceed the screen width int rightEdge = menu.getX() + menu.getWidth(); @@ -2961,7 +3132,7 @@ public class TApplication implements Runnable { TMenu toolMenu = addMenu(i18n.getString("toolMenuTitle")); toolMenu.addDefaultItem(TMenu.MID_REPAINT); toolMenu.addDefaultItem(TMenu.MID_VIEW_IMAGE); - toolMenu.addDefaultItem(TMenu.MID_CHANGE_FONT); + toolMenu.addDefaultItem(TMenu.MID_SCREEN_OPTIONS); TStatusBar toolStatusBar = toolMenu.newStatusBar(i18n. getString("toolMenuStatus")); toolStatusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help")); @@ -2975,9 +3146,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")); @@ -3045,6 +3215,64 @@ 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_OPEN_CSV, false); + 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 ------------------------------------------------------ // ------------------------------------------------------------------------