X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTApplication.java;h=28e35091ded6e1ef006190574e945c0426c41057;hb=c4cefaa04ec122fc02efb6542451a31fdf722c32;hp=658e50bbc4bf8d029c121c9c00f59bccda1564a2;hpb=f528c340199b03ccc2e0d2835b6156562686a202;p=nikiroo-utils.git diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 658e50b..28e3509 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -29,6 +29,7 @@ package jexer; import java.io.File; +import java.io.FileInputStream; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; @@ -47,7 +48,9 @@ import java.util.ResourceBundle; import jexer.bits.Cell; import jexer.bits.CellAttributes; +import jexer.bits.Clipboard; import jexer.bits.ColorTheme; +import jexer.bits.StringUtils; import jexer.event.TCommandEvent; import jexer.event.TInputEvent; import jexer.event.TKeypressEvent; @@ -60,6 +63,8 @@ import jexer.backend.Screen; import jexer.backend.SwingBackend; import jexer.backend.ECMA48Backend; import jexer.backend.TWindowBackend; +import jexer.help.HelpFile; +import jexer.help.Topic; import jexer.menu.TMenu; import jexer.menu.TMenuItem; import jexer.menu.TSubMenu; @@ -132,6 +137,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. */ @@ -142,6 +152,11 @@ public class TApplication implements Runnable { */ private Backend backend; + /** + * The clipboard for copy and paste. + */ + private Clipboard clipboard = new Clipboard(); + /** * Actual mouse coordinate X. */ @@ -152,16 +167,6 @@ public class TApplication implements Runnable { */ private int mouseY; - /** - * Old version of mouse coordinate X. - */ - private int oldMouseX; - - /** - * Old version mouse coordinate Y. - */ - private int oldMouseY; - /** * Old drawn version of mouse coordinate X. */ @@ -234,11 +239,6 @@ public class TApplication implements Runnable { */ private List windows; - /** - * The currently acive window. - */ - private TWindow activeWindow = null; - /** * Timers that are being ticked. */ @@ -264,7 +264,7 @@ public class TApplication implements Runnable { * constant. Someday it would be nice to have a multi-line menu or * toolbars. */ - private static final int desktopTop = 1; + private int desktopTop = 1; /** * Y coordinate of the bottom edge of the desktop. @@ -284,15 +284,81 @@ public class TApplication implements Runnable { private boolean focusFollowsMouse = false; /** - * The images that might be displayed. Note package private access. + * If true, display a text-based mouse cursor. */ - private List images; + private boolean textMouse = true; + + /** + * If true, hide the mouse after typing a keystroke. + */ + private boolean hideMouseWhenTyping = false; + + /** + * If true, the mouse should not be displayed because a keystroke was + * typed. + */ + private boolean typingHidMouse = false; + + /** + * If true, hide the status bar. + */ + private boolean hideStatusBar = false; + + /** + * If true, hide the menu bar. + */ + private boolean hideMenuBar = false; /** * The list of commands to run before the next I/O check. */ private List invokeLaters = new LinkedList(); + /** + * The last time the screen was resized. + */ + private long screenResizeTime = 0; + + /** + * If true, screen selection is a rectangle. + */ + private boolean screenSelectionRectangle = false; + + /** + * If true, the mouse is dragging a screen selection. + */ + private boolean inScreenSelection = false; + + /** + * Screen selection starting X. + */ + private int screenSelectionX0; + + /** + * Screen selection starting Y. + */ + private int screenSelectionY0; + + /** + * Screen selection ending X. + */ + private int screenSelectionX1; + + /** + * Screen selection ending Y. + */ + private int screenSelectionY1; + + /** + * The help file data. Note package private access. + */ + HelpFile helpFile; + + /** + * The stack of help topics. Note package private access. + */ + ArrayList helpTopics = new ArrayList(); + /** * WidgetEventHandler is the main event consumer loop. There are at most * two such threads in existence: the primary for normal case and a @@ -455,6 +521,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 ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -595,8 +749,32 @@ public class TApplication implements Runnable { * Finish construction once the backend is set. */ private void TApplicationImpl() { + // Text block mouse option + if (System.getProperty("jexer.textMouse", "true").equals("false")) { + textMouse = false; + } + + // Hide mouse when typing option + if (System.getProperty("jexer.hideMouseWhenTyping", + "false").equals("true")) { + + hideMouseWhenTyping = true; + } + + // Hide status bar option + if (System.getProperty("jexer.hideStatusBar", + "false").equals("true")) { + hideStatusBar = true; + } + + // Hide menu bar option + if (System.getProperty("jexer.hideMenuBar", "false").equals("true")) { + hideMenuBar = true; + } + theme = new ColorTheme(); - desktopBottom = getScreen().getHeight() - 1; + desktopTop = (hideMenuBar ? 0 : 1); + desktopBottom = getScreen().getHeight() - 1 + (hideStatusBar ? 1 : 0); fillEventQueue = new LinkedList(); drainEventQueue = new LinkedList(); windows = new LinkedList(); @@ -606,7 +784,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. @@ -629,6 +806,28 @@ public class TApplication implements Runnable { ); } } + + // Load the help system + invokeLater(new Runnable() { + /* + * This isn't the best solution. But basically if a TApplication + * subclass constructor throws and needs to use TExceptionDialog, + * it may end up at the bottom of the window stack with a bunch + * of modal windows on top of it if said constructors spawn their + * windows also via invokeLater(). But if they don't do that, + * and instead just conventionally construct their windows, then + * this exception dialog will end up on top where it should be. + */ + public void run() { + try { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + helpFile = new HelpFile(); + helpFile.load(loader.getResourceAsStream("help.xml")); + } catch (Exception e) { + new TExceptionDialog(TApplication.this, e); + } + } + }); } // ------------------------------------------------------------------------ @@ -641,6 +840,10 @@ public class TApplication implements Runnable { 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(); @@ -717,13 +920,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(); + // Close the desktop. + if (desktop != null) { + setDesktop(null); + } + // Give the overarching application an opportunity to release // resources. onExit(); @@ -754,6 +959,15 @@ public class TApplication implements Runnable { return true; } + if (command.equals(cmHelp)) { + if (getActiveWindow() != null) { + new THelpWindow(this, getActiveWindow().getHelpTopic()); + } else { + new THelpWindow(this); + } + return true; + } + if (command.equals(cmShell)) { openTerminal(0, 0, TWindow.RESIZABLE); return true; @@ -772,7 +986,7 @@ public class TApplication implements Runnable { return true; } - if (command.equals(cmMenu)) { + if (command.equals(cmMenu) && (hideMenuBar == false)) { if (!modalWindowActive() && (activeMenu == null)) { if (menus.size() > 0) { menus.get(0).setActive(true); @@ -805,6 +1019,62 @@ public class TApplication implements Runnable { return true; } + if (menu.getId() == TMenu.MID_HELP_HELP) { + new THelpWindow(this, THelpWindow.HELP_HELP); + return true; + } + + if (menu.getId() == TMenu.MID_HELP_CONTENTS) { + new THelpWindow(this, helpFile.getTableOfContents()); + return true; + } + + if (menu.getId() == TMenu.MID_HELP_INDEX) { + new THelpWindow(this, helpFile.getIndex()); + return true; + } + + if (menu.getId() == TMenu.MID_HELP_SEARCH) { + TInputBox inputBox = inputBox(i18n. + getString("searchHelpInputBoxTitle"), + i18n.getString("searchHelpInputBoxCaption"), "", + TInputBox.Type.OKCANCEL); + if (inputBox.isOk()) { + new THelpWindow(this, + helpFile.getSearchResults(inputBox.getText())); + } + return true; + } + + if (menu.getId() == TMenu.MID_HELP_PREVIOUS) { + if (helpTopics.size() > 1) { + Topic previous = helpTopics.remove(helpTopics.size() - 2); + helpTopics.remove(helpTopics.size() - 1); + new THelpWindow(this, previous); + } else { + new THelpWindow(this, helpFile.getTableOfContents()); + } + return true; + } + + if (menu.getId() == TMenu.MID_HELP_ACTIVE_FILE) { + try { + List filters = new ArrayList(); + filters.add("^.*\\.[Xx][Mm][Ll]$"); + String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN, + filters); + if (filename != null) { + helpTopics = new ArrayList(); + helpFile = new HelpFile(); + helpFile.load(new FileInputStream(filename)); + } + } catch (Exception e) { + // Show this exception to the user. + new TExceptionDialog(this, e); + } + return true; + } + if (menu.getId() == TMenu.MID_SHELL) { openTerminal(0, 0, TWindow.RESIZABLE); return true; @@ -835,10 +1105,28 @@ 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; } + + if (menu.getId() == TMenu.MID_CUT) { + postMenuEvent(new TCommandEvent(cmCut)); + return true; + } + if (menu.getId() == TMenu.MID_COPY) { + postMenuEvent(new TCommandEvent(cmCopy)); + return true; + } + if (menu.getId() == TMenu.MID_PASTE) { + postMenuEvent(new TCommandEvent(cmPaste)); + return true; + } + if (menu.getId() == TMenu.MID_CLEAR) { + postMenuEvent(new TCommandEvent(cmClear)); + return true; + } + return false; } @@ -857,6 +1145,7 @@ public class TApplication implements Runnable { && !keypress.getKey().isCtrl() && (activeMenu == null) && !modalWindowActive() + && (hideMenuBar == false) ) { assert (subMenus.size() == 0); @@ -884,6 +1173,48 @@ public class TApplication implements Runnable { Thread.currentThread() + " finishEventProcessing()\n"); } + // See if we need to enable/disable the edit menu. + EditMenuUser widget = null; + if (activeMenu == null) { + TWindow activeWindow = getActiveWindow(); + if (activeWindow != null) { + if (activeWindow.getActiveChild() instanceof EditMenuUser) { + widget = (EditMenuUser) activeWindow.getActiveChild(); + } + } else if (desktop != null) { + if (desktop.getActiveChild() instanceof EditMenuUser) { + widget = (EditMenuUser) desktop.getActiveChild(); + } + } + if (widget == null) { + disableMenuItem(TMenu.MID_CUT); + disableMenuItem(TMenu.MID_COPY); + disableMenuItem(TMenu.MID_PASTE); + disableMenuItem(TMenu.MID_CLEAR); + } else { + if (widget.isEditMenuCut()) { + enableMenuItem(TMenu.MID_CUT); + } else { + disableMenuItem(TMenu.MID_CUT); + } + if (widget.isEditMenuCopy()) { + enableMenuItem(TMenu.MID_COPY); + } else { + disableMenuItem(TMenu.MID_COPY); + } + if (widget.isEditMenuPaste()) { + enableMenuItem(TMenu.MID_PASTE); + } else { + disableMenuItem(TMenu.MID_PASTE); + } + if (widget.isEditMenuClear()) { + enableMenuItem(TMenu.MID_CLEAR); + } else { + disableMenuItem(TMenu.MID_CLEAR); + } + } + } + // Process timers and call doIdle()'s doIdle(); @@ -892,6 +1223,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"); @@ -933,17 +1267,24 @@ 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; + if (hideStatusBar) { + desktopBottom++; + } mouseX = 0; mouseY = 0; - oldMouseX = 0; - oldMouseY = 0; } if (desktop != null) { - desktop.setDimensions(0, 0, resize.getWidth(), - resize.getHeight() - 1); + desktop.setDimensions(0, desktopTop, resize.getWidth(), + (desktopBottom - desktopTop)); desktop.onResize(resize); } @@ -983,12 +1324,40 @@ public class TApplication implements Runnable { // Special application-wide events ----------------------------------- + if (event instanceof TKeypressEvent) { + if (hideMouseWhenTyping) { + typingHidMouse = true; + } + } + // Peek at the mouse position if (event instanceof TMouseEvent) { + typingHidMouse = false; + TMouseEvent mouse = (TMouseEvent) event; + if (mouse.isMouse1() && (mouse.isShift() || mouse.isCtrl())) { + // Screen selection. + if (inScreenSelection) { + screenSelectionX1 = mouse.getX(); + screenSelectionY1 = mouse.getY(); + } else { + inScreenSelection = true; + screenSelectionX0 = mouse.getX(); + screenSelectionY0 = mouse.getY(); + screenSelectionX1 = mouse.getX(); + screenSelectionY1 = mouse.getY(); + screenSelectionRectangle = mouse.isCtrl(); + } + } else { + if (inScreenSelection) { + getScreen().copySelection(clipboard, screenSelectionX0, + screenSelectionY0, screenSelectionX1, screenSelectionY1, + screenSelectionRectangle); + } + inScreenSelection = false; + } + if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) { - oldMouseX = mouseX; - oldMouseY = mouseY; mouseX = mouse.getX(); mouseY = mouse.getY(); } else { @@ -1006,7 +1375,8 @@ public class TApplication implements Runnable { mouse.getAbsoluteX(), mouse.getAbsoluteY(), mouse.isMouse1(), mouse.isMouse2(), mouse.isMouse3(), - mouse.isMouseWheelUp(), mouse.isMouseWheelDown()); + mouse.isMouseWheelUp(), mouse.isMouseWheelDown(), + mouse.isAlt(), mouse.isCtrl(), mouse.isShift()); } else { // The first click of a potential double-click. @@ -1064,6 +1434,7 @@ public class TApplication implements Runnable { // shortcutted by the active window, and if so dispatch the menu // event. boolean windowWillShortcut = false; + TWindow activeWindow = getActiveWindow(); if (activeWindow != null) { assert (activeWindow.isShown()); if (activeWindow.isShortcutKeypress(keypress.getKey())) { @@ -1108,7 +1479,7 @@ public class TApplication implements Runnable { // Dispatch events to the active window ------------------------------- boolean dispatchToDesktop = true; - TWindow window = activeWindow; + TWindow window = getActiveWindow(); if (window != null) { assert (window.isActive()); assert (window.isShown()); @@ -1130,6 +1501,8 @@ public class TApplication implements Runnable { } } else if (event instanceof TKeypressEvent) { dispatchToDesktop = false; + } else if (event instanceof TMenuEvent) { + dispatchToDesktop = false; } if (debugEvents) { @@ -1170,10 +1543,10 @@ public class TApplication implements Runnable { // Peek at the mouse position if (event instanceof TMouseEvent) { + typingHidMouse = false; + TMouseEvent mouse = (TMouseEvent) event; if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) { - oldMouseX = mouseX; - oldMouseY = mouseY; mouseX = mouse.getX(); mouseY = mouse.getY(); } else { @@ -1191,7 +1564,8 @@ public class TApplication implements Runnable { mouse.getAbsoluteX(), mouse.getAbsoluteY(), mouse.isMouse1(), mouse.isMouse2(), mouse.isMouse3(), - mouse.isMouseWheelUp(), mouse.isMouseWheelDown()); + mouse.isMouseWheelUp(), mouse.isMouseWheelDown(), + mouse.isAlt(), mouse.isCtrl(), mouse.isShift()); } else { // The first click of a potential double-click. @@ -1299,13 +1673,17 @@ public class TApplication implements Runnable { desktop.onIdle(); } - // Run any invokeLaters + // Run any invokeLaters. We make a copy, and run that, because one + // of these Runnables might add call TApplication.invokeLater(). + List invokes = new ArrayList(); synchronized (invokeLaters) { - for (Runnable invoke: invokeLaters) { - invoke.run(); - } + invokes.addAll(invokeLaters); invokeLaters.clear(); } + for (Runnable invoke: invokes) { + invoke.run(); + } + doRepaint(); } @@ -1329,6 +1707,19 @@ public class TApplication implements Runnable { } } + /** + * Wake the sleeping screen handler. + */ + private void wakeScreenHandler() { + if (!started) { + return; + } + + synchronized (screenHandler) { + screenHandler.notify(); + } + } + // ------------------------------------------------------------------------ // TApplication ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -1394,6 +1785,15 @@ public class TApplication implements Runnable { return theme; } + /** + * Get the clipboard. + * + * @return the clipboard + */ + public final Clipboard getClipboard() { + return clipboard; + } + /** * Repaint the screen on the next update. */ @@ -1428,6 +1828,8 @@ public class TApplication implements Runnable { */ public final void setDesktop(final TDesktop desktop) { if (this.desktop != null) { + this.desktop.onPreClose(); + this.desktop.onUnfocus(); this.desktop.onClose(); } this.desktop = desktop; @@ -1448,7 +1850,12 @@ public class TApplication implements Runnable { * @return the active window, or null if it is not set */ public final TWindow getActiveWindow() { - return activeWindow; + for (TWindow window: windows) { + if (window.isShown() && window.isActive()) { + return window; + } + } + return null; } /** @@ -1489,7 +1896,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.1"; + version = "1.0.0"; } messageBox(i18n.getString("aboutDialogTitle"), MessageFormat.format(i18n.getString("aboutDialogText"), version), @@ -1534,14 +1941,16 @@ public class TApplication implements Runnable { // ------------------------------------------------------------------------ /** - * Invert the cell color at a position. This is used to track the mouse. + * Draw the text mouse at position. * * @param x column position * @param y row position */ - private void invertCell(final int x, final int y) { + private void drawTextMouse(final int x, final int y) { + TWindow activeWindow = getActiveWindow(); + if (debugThreads) { - System.err.printf("%d %s invertCell() %d %d\n", + System.err.printf("%d %s drawTextMouse() %d %d\n", System.currentTimeMillis(), Thread.currentThread(), x, y); if (activeWindow != null) { @@ -1563,22 +1972,20 @@ public class TApplication implements Runnable { } } - Cell cell = getScreen().getCharXY(x, y); - if (cell.isImage()) { - cell.invertImage(); - } else { - 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); + // If this cell is on top of the desktop, and the desktop has + // requested a hidden mouse, bail out. + if ((desktop != null) && (activeWindow == null) && (activeMenu == null)) { + if ((desktop.hasHiddenMouse() == true) + && (x > desktop.getX()) + && (x < desktop.getX() + desktop.getWidth() - 1) + && (y > desktop.getY()) + && (y < desktop.getY() + desktop.getHeight() - 1) + ) { + return; } } - getScreen().putCharXY(x, y, cell); + + getScreen().invertCell(x, y); } /** @@ -1617,7 +2024,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. @@ -1638,14 +2045,22 @@ public class TApplication implements Runnable { } } - // Draw mouse at the new position. - invertCell(mouseX, mouseY); + if (inScreenSelection) { + getScreen().setSelection(screenSelectionX0, + screenSelectionY0, screenSelectionX1, screenSelectionY1, + screenSelectionRectangle); + } + + if ((textMouse == true) && (typingHidMouse == false)) { + // Draw mouse at the new position. + drawTextMouse(mouseX, mouseY); + } oldDrawnMouseX = mouseX; oldDrawnMouseY = mouseY; } - if ((images.size() > 0) || getScreen().isDirty()) { - backend.flushScreen(); + if (getScreen().isDirty()) { + screenHandler.setDirty(); } return; } @@ -1680,63 +2095,73 @@ public class TApplication implements Runnable { } } - // Draw the blank menubar line - reset the screen clipping first so - // it won't trim it out. - getScreen().resetClipping(); - getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ', - theme.getColor("tmenu")); - // Now draw the menus. - int x = 1; - for (TMenu menu: menus) { - CellAttributes menuColor; - CellAttributes menuMnemonicColor; - if (menu.isActive()) { - menuIsActive = true; - menuColor = theme.getColor("tmenu.highlighted"); - menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted"); - topLevel = menu; - } else { - menuColor = theme.getColor("tmenu"); - menuMnemonicColor = theme.getColor("tmenu.mnemonic"); - } - // Draw the menu title - getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ', - 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.isActive()) { - ((TWindow) menu).drawChildren(); - // Reset the screen clipping so we can draw the next title. - getScreen().resetClipping(); - } - x += menu.getTitle().length() + 2; - } + if (hideMenuBar == false) { - for (TMenu menu: subMenus) { - // Reset the screen clipping so we can draw the next sub-menu. + // Draw the blank menubar line - reset the screen clipping first + // so it won't trim it out. getScreen().resetClipping(); - ((TWindow) menu).drawChildren(); + getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ', + theme.getColor("tmenu")); + // Now draw the menus. + int x = 1; + for (TMenu menu: menus) { + CellAttributes menuColor; + CellAttributes menuMnemonicColor; + if (menu.isActive()) { + menuIsActive = true; + menuColor = theme.getColor("tmenu.highlighted"); + menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted"); + topLevel = menu; + } else { + menuColor = theme.getColor("tmenu"); + menuMnemonicColor = theme.getColor("tmenu.mnemonic"); + } + // Draw the menu title + getScreen().hLineXY(x, 0, + StringUtils.width(menu.getTitle()) + 2, ' ', menuColor); + getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor); + // Draw the highlight character + getScreen().putCharXY(x + 1 + + menu.getMnemonic().getScreenShortcutIdx(), + 0, menu.getMnemonic().getShortcut(), menuMnemonicColor); + + if (menu.isActive()) { + ((TWindow) menu).drawChildren(); + // Reset the screen clipping so we can draw the next + // title. + getScreen().resetClipping(); + } + x += StringUtils.width(menu.getTitle()) + 2; + } + + for (TMenu menu: subMenus) { + // Reset the screen clipping so we can draw the next + // sub-menu. + getScreen().resetClipping(); + ((TWindow) menu).drawChildren(); + } } getScreen().resetClipping(); - // 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); + if (hideStatusBar == false) { + // Draw the status bar of the top-level window + TStatusBar statusBar = null; + if (topLevel != null) { + if (topLevel.isShown()) { + 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 @@ -1746,7 +2171,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) { @@ -1765,18 +2190,43 @@ public class TApplication implements Runnable { getScreen().unsetImageRow(mouseY); } } - invertCell(mouseX, mouseY); + + if (inScreenSelection) { + getScreen().setSelection(screenSelectionX0, screenSelectionY0, + screenSelectionX1, screenSelectionY1, screenSelectionRectangle); + } + + if ((textMouse == true) && (typingHidMouse == false)) { + drawTextMouse(mouseX, mouseY); + } oldDrawnMouseX = mouseX; oldDrawnMouseY = mouseY; // Place the cursor if it is visible if (!menuIsActive) { + + int visibleWindowCount = 0; + for (TWindow window: sorted) { + if (window.isShown()) { + visibleWindowCount++; + } + } + if (visibleWindowCount == 0) { + // No windows are visible, only the desktop. Allow it to + // have the cursor. + if (desktop != null) { + sorted.add(desktop); + } + } + TWidget activeWidget = null; if (sorted.size() > 0) { activeWidget = sorted.get(sorted.size() - 1).getActiveChild(); + int cursorClipTop = desktopTop; + int cursorClipBottom = desktopBottom; if (activeWidget.isCursorVisible()) { - if ((activeWidget.getCursorAbsoluteY() < desktopBottom) - && (activeWidget.getCursorAbsoluteY() > desktopTop) + if ((activeWidget.getCursorAbsoluteY() <= cursorClipBottom) + && (activeWidget.getCursorAbsoluteY() >= cursorClipTop) ) { getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(), @@ -1796,15 +2246,9 @@ public class TApplication implements Runnable { getScreen().hideCursor(); } - // Flush the screen contents - if ((images.size() > 0) || getScreen().isDirty()) { - if (debugThreads) { - System.err.printf("%d %s backend.flushScreen()\n", - System.currentTimeMillis(), Thread.currentThread()); - } - backend.flushScreen(); + if (getScreen().isDirty()) { + screenHandler.setDirty(); } - repaint = false; } @@ -1893,7 +2337,7 @@ public class TApplication implements Runnable { * * @param window the window to become the new active window */ - public void activateWindow(final TWindow window) { + public final void activateWindow(final TWindow window) { if (hasWindow(window) == false) { /* * Someone has a handle to a window I don't have. Ignore this @@ -1902,71 +2346,61 @@ public class TApplication implements Runnable { return; } - // Whatever window might be moving/dragging, stop it now. - for (TWindow w: windows) { - if (w.inMovements()) { - w.stopMovements(); - } - } - - assert (windows.size() > 0); - - if (window.isHidden()) { - // Unhiding will also activate. - showWindow(window); + if (modalWindowActive() && !window.isModal()) { + // Do not activate a non-modal on top of a modal. 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(); + synchronized (windows) { + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); + } } - assert (window.isActive()); - assert (activeWindow == window); - return; - } + assert (windows.size() > 0); - if (activeWindow == window) { - assert (window.isActive()); + if (window.isHidden()) { + // Unhiding will also activate. + showWindow(window); + return; + } + assert (window.isShown()); - // Window is already active, do nothing. - return; - } + if (windows.size() == 1) { + assert (window == windows.get(0)); + window.setZ(0); + window.setActive(true); + window.onFocus(); + return; + } - assert (!window.isActive()); - if (activeWindow != null) { - // TODO: see if this assertion is really necessary. - // assert (activeWindow.getZ() == 0); + if (getActiveWindow() == window) { + assert (window.isActive()); + + // Window is already active, do nothing. + return; + } - activeWindow.setActive(false); + assert (!window.isActive()); - // Increment every window Z that is on top of window + window.setZ(-1); + Collections.sort(windows); + int newZ = 0; for (TWindow w: windows) { - if (w == window) { - continue; - } - if (w.getZ() < window.getZ()) { - w.setZ(w.getZ() + 1); + w.setZ(newZ); + newZ++; + if ((w != window) && w.isActive()) { + w.onUnfocus(); } + w.setActive(false); } + window.setActive(true); + window.onFocus(); + + } // synchronized (windows) - // Unset activeWindow now before unfocus, so that a window - // lifecycle change inside onUnfocus() doesn't call - // switchWindow() and lead to a stack overflow. - TWindow oldActiveWindow = activeWindow; - activeWindow = null; - oldActiveWindow.onUnfocus(); - } - activeWindow = window; - activeWindow.setZ(0); - activeWindow.setActive(true); - activeWindow.onFocus(); return; } @@ -1984,28 +2418,39 @@ public class TApplication implements Runnable { return; } - // Whatever window might be moving/dragging, stop it now. - for (TWindow w: windows) { - if (w.inMovements()) { - w.stopMovements(); + synchronized (windows) { + + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); + } } - } - assert (windows.size() > 0); + assert (windows.size() > 0); - if (!window.hidden) { - if (window == activeWindow) { - if (shownWindowCount() > 1) { - switchWindow(true); - } else { - activeWindow = null; - window.setActive(false); - window.onUnfocus(); - } + if (window.hidden) { + return; } + + window.setActive(false); window.hidden = true; window.onHide(); - } + + TWindow activeWindow = null; + for (TWindow w: windows) { + if (w.isShown()) { + activeWindow = w; + break; + } + } + assert (activeWindow != window); + if (activeWindow != null) { + activateWindow(activeWindow); + } + + } // synchronized (windows) + } /** @@ -2022,25 +2467,16 @@ public class TApplication implements Runnable { return; } - // Whatever window might be moving/dragging, stop it now. - for (TWindow w: windows) { - if (w.inMovements()) { - w.stopMovements(); - } - } - - assert (windows.size() > 0); - if (window.hidden) { window.hidden = false; window.onShow(); activateWindow(window); } + } /** - * Close window. Note that the window's destructor is NOT called by this - * method, instead the GC is assumed to do the cleanup. + * Close window. * * @param window the window to remove */ @@ -2058,23 +2494,16 @@ public class TApplication implements Runnable { window.onPreClose(); synchronized (windows) { - // Whatever window might be moving/dragging, stop it now. - for (TWindow w: windows) { - if (w.inMovements()) { - w.stopMovements(); - } - } - int z = window.getZ(); - window.setZ(-1); + window.stopMovements(); window.onUnfocus(); windows.remove(window); Collections.sort(windows); - activeWindow = null; - int newZ = 0; - boolean foundNextWindow = false; + TWindow nextWindow = null; + int newZ = 0; for (TWindow w: windows) { + w.stopMovements(); w.setZ(newZ); newZ++; @@ -2082,22 +2511,22 @@ public class TApplication implements Runnable { if (w.isHidden()) { continue; } - - if (foundNextWindow == false) { - foundNextWindow = true; - w.setActive(true); - w.onFocus(); - assert (activeWindow == null); - activeWindow = w; - continue; + if (nextWindow == null) { + nextWindow = w; + } else { + if (w.isActive()) { + w.setActive(false); + w.onUnfocus(); + } } + } - if (w.isActive()) { - w.setActive(false); - w.onUnfocus(); - } + if (nextWindow != null) { + nextWindow.setActive(true); + nextWindow.onFocus(); } - } + + } // synchronized (windows) // Perform window cleanup window.onClose(); @@ -2115,7 +2544,8 @@ public class TApplication implements Runnable { synchronized (secondaryEventHandler) { secondaryEventHandler.notify(); } - } + + } // synchronized (windows) // Permit desktop to be active if it is the only thing left. if (desktop != null) { @@ -2136,53 +2566,50 @@ public class TApplication implements Runnable { if (shownWindowCount() < 2) { return; } - assert (activeWindow != null); + + if (modalWindowActive()) { + // Do not switch if a window is modal + return; + } synchronized (windows) { - // Whatever window might be moving/dragging, stop it now. - for (TWindow w: windows) { - if (w.inMovements()) { - w.stopMovements(); - } - } - // Swap z/active between active window and the next in the list - int activeWindowI = -1; - for (int i = 0; i < windows.size(); i++) { - if (windows.get(i) == activeWindow) { - assert (activeWindow.isActive()); - activeWindowI = i; - break; + TWindow window = windows.get(0); + do { + assert (window != null); + if (forward) { + window.setZ(windows.size()); } else { - assert (!windows.get(0).isActive()); + TWindow lastWindow = windows.get(windows.size() - 1); + lastWindow.setZ(-1); } - } - assert (activeWindowI >= 0); - // Do not switch if a window is modal - if (activeWindow.isModal()) { - return; - } - - int nextWindowI = activeWindowI; - for (;;) { - if (forward) { - nextWindowI++; - nextWindowI %= windows.size(); - } else { - nextWindowI--; - if (nextWindowI < 0) { - nextWindowI = windows.size() - 1; - } + Collections.sort(windows); + int newZ = 0; + for (TWindow w: windows) { + w.setZ(newZ); + newZ++; } - if (windows.get(nextWindowI).isShown()) { - activateWindow(windows.get(nextWindowI)); - break; + window = windows.get(0); + } while (!window.isShown()); + + // The next visible window is now on top. Renumber the list. + for (TWindow w: windows) { + w.stopMovements(); + if ((w != window) && w.isActive()) { + assert (w.isShown()); + w.setActive(false); + w.onUnfocus(); } } - } // synchronized (windows) + // Next visible window is on top. + assert (window.isShown()); + window.setActive(true); + window.onFocus(); + + } // synchronized (windows) } /** @@ -2232,18 +2659,19 @@ public class TApplication implements Runnable { } w.setZ(w.getZ() + 1); } - } - windows.add(window); - if (window.isShown()) { - activeWindow = window; - activeWindow.setZ(0); - activeWindow.setActive(true); - activeWindow.onFocus(); + window.setZ(0); + window.setActive(true); + window.onFocus(); + windows.add(0, window); + } else { + window.setZ(windows.size()); + windows.add(window); } if (((window.flags & TWindow.CENTERED) == 0) && ((window.flags & TWindow.ABSOLUTEXY) == 0) && (smartWindowPlacement == true) + && (!(window instanceof TDesktop)) ) { doSmartPlacement(window); @@ -2254,6 +2682,7 @@ public class TApplication implements Runnable { if (desktop != null) { desktop.setActive(false); } + } /** @@ -2281,6 +2710,7 @@ public class TApplication implements Runnable { * @return true if the active window is overriding the menu */ private boolean overrideMenuWindowActive() { + TWindow activeWindow = getActiveWindow(); if (activeWindow != null) { if (activeWindow.hasOverriddenMenu()) { return true; @@ -2529,46 +2959,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 ------------------------------------------------------- // ------------------------------------------------------------------------ @@ -2622,6 +3012,7 @@ public class TApplication implements Runnable { && (!modalWindowActive()) && (!overrideMenuWindowActive()) && (mouse.getAbsoluteY() == 0) + && (hideMenuBar == false) ) { for (TMenu menu: subMenus) { @@ -2633,7 +3024,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; @@ -2649,6 +3040,7 @@ public class TApplication implements Runnable { && (mouse.isMouse1()) && (activeMenu != null) && (mouse.getAbsoluteY() == 0) + && (hideMenuBar == false) ) { TMenu oldMenu = activeMenu; @@ -2661,7 +3053,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; @@ -2689,7 +3081,6 @@ public class TApplication implements Runnable { || (mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) ) { synchronized (windows) { - Collections.sort(windows); if (windows.get(0).isModal()) { // Modal windows don't switch return; @@ -2704,25 +3095,7 @@ public class TApplication implements Runnable { } 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()); - if (activeWindow != null) { - activeWindow.onUnfocus(); - activeWindow.setActive(false); - activeWindow.setZ(window.getZ()); - } - activeWindow = window; - window.setZ(0); - window.setActive(true); - window.onFocus(); + activateWindow(window); return; } } @@ -2817,6 +3190,7 @@ public class TApplication implements Runnable { */ public final void switchMenu(final boolean forward) { assert (activeMenu != null); + assert (hideMenuBar == false); for (TMenu menu: subMenus) { menu.setActive(false); @@ -2946,7 +3320,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(); @@ -3026,7 +3400,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")); @@ -3056,10 +3430,13 @@ public class TApplication implements Runnable { */ public final TMenu addEditMenu() { TMenu editMenu = addMenu(i18n.getString("editMenuTitle")); - editMenu.addDefaultItem(TMenu.MID_CUT); - editMenu.addDefaultItem(TMenu.MID_COPY); - editMenu.addDefaultItem(TMenu.MID_PASTE); - editMenu.addDefaultItem(TMenu.MID_CLEAR); + editMenu.addDefaultItem(TMenu.MID_UNDO, false); + editMenu.addDefaultItem(TMenu.MID_REDO, false); + editMenu.addSeparator(); + editMenu.addDefaultItem(TMenu.MID_CUT, false); + editMenu.addDefaultItem(TMenu.MID_COPY, false); + editMenu.addDefaultItem(TMenu.MID_PASTE, false); + editMenu.addDefaultItem(TMenu.MID_CLEAR, false); TStatusBar statusBar = editMenu.newStatusBar(i18n. getString("editMenuStatus")); statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));