From: Niki Roo Date: Thu, 2 Jan 2020 15:04:07 +0000 (+0100) Subject: Merge branch 'upstream' into subtree X-Git-Url: http://git.nikiroo.be/?p=nikiroo-utils.git;a=commitdiff_plain;h=e6bb1700749980e69b5e913acbfd276f129c24dc;hp=-c Merge branch 'upstream' into subtree --- e6bb1700749980e69b5e913acbfd276f129c24dc diff --combined TApplication.java index 9d27c10,28e3509..28e3509 --- a/TApplication.java +++ b/TApplication.java @@@ -29,6 -29,7 +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,6 -48,7 +48,7 @@@ 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; @@@ -61,6 -63,8 +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; @@@ -148,6 -152,11 +152,11 @@@ public class TApplication implements Ru */ private Backend backend; + /** + * The clipboard for copy and paste. + */ + private Clipboard clipboard = new Clipboard(); + /** * Actual mouse coordinate X. */ @@@ -158,16 -167,6 +167,6 @@@ */ 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. */ @@@ -240,11 -239,6 +239,6 @@@ */ private List windows; - /** - * The currently acive window. - */ - private TWindow activeWindow = null; - /** * Timers that are being ticked. */ @@@ -325,6 -319,46 +319,46 @@@ */ 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 @@@ -773,6 -807,27 +807,27 @@@ } } + // 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); + } + } + }); } // ------------------------------------------------------------------------ @@@ -904,6 -959,15 +959,15 @@@ 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; @@@ -955,6 -1019,62 +1019,62 @@@ 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; @@@ -989,6 -1109,24 +1109,24 @@@ 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; } @@@ -1035,6 -1173,48 +1173,48 @@@ 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(); @@@ -1101,8 -1281,6 +1281,6 @@@ } mouseX = 0; mouseY = 0; - oldMouseX = 0; - oldMouseY = 0; } if (desktop != null) { desktop.setDimensions(0, desktopTop, resize.getWidth(), @@@ -1157,9 -1335,29 +1335,29 @@@ 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 { @@@ -1177,7 -1375,8 +1375,8 @@@ 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. @@@ -1235,6 -1434,7 +1434,7 @@@ // 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())) { @@@ -1279,7 -1479,7 +1479,7 @@@ // Dispatch events to the active window ------------------------------- boolean dispatchToDesktop = true; - TWindow window = activeWindow; + TWindow window = getActiveWindow(); if (window != null) { assert (window.isActive()); assert (window.isShown()); @@@ -1347,8 -1547,6 +1547,6 @@@ TMouseEvent mouse = (TMouseEvent) event; if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) { - oldMouseX = mouseX; - oldMouseY = mouseY; mouseX = mouse.getX(); mouseY = mouse.getY(); } else { @@@ -1366,7 -1564,8 +1564,8 @@@ 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. @@@ -1474,13 -1673,17 +1673,17 @@@ 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(); } @@@ -1582,6 -1785,15 +1785,15 @@@ return theme; } + /** + * Get the clipboard. + * + * @return the clipboard + */ + public final Clipboard getClipboard() { + return clipboard; + } + /** * Repaint the screen on the next update. */ @@@ -1638,7 -1850,12 +1850,12 @@@ * @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; } /** @@@ -1679,7 -1896,7 +1896,7 @@@ String version = getClass().getPackage().getImplementationVersion(); if (version == null) { // This is Java 9+, use a hardcoded string here. - version = "0.3.2"; + version = "1.0.0"; } messageBox(i18n.getString("aboutDialogTitle"), MessageFormat.format(i18n.getString("aboutDialogText"), version), @@@ -1724,27 -1941,16 +1941,16 @@@ // ------------------------------------------------------------------------ /** - * Invert the cell color at a position. This is used to track the mouse. - * - * @param x column position - * @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. + * Draw the text mouse at position. * * @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) { + 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) { @@@ -1779,44 -1985,7 +1985,7 @@@ } } - Cell cell = getScreen().getCharXY(x, y); - if (cell.isImage()) { - cell.invertImage(); - } - 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().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; - } - } - } - if (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); - } - } - } + getScreen().invertCell(x, y); } /** @@@ -1876,9 -2045,15 +2045,15 @@@ } } + if (inScreenSelection) { + getScreen().setSelection(screenSelectionX0, + screenSelectionY0, screenSelectionX1, screenSelectionY1, + screenSelectionRectangle); + } + if ((textMouse == true) && (typingHidMouse == false)) { // Draw mouse at the new position. - invertCell(mouseX, mouseY); + drawTextMouse(mouseX, mouseY); } oldDrawnMouseX = mouseX; @@@ -1972,7 -2147,9 +2147,9 @@@ // Draw the status bar of the top-level window TStatusBar statusBar = null; if (topLevel != null) { - statusBar = topLevel.getStatusBar(); + if (topLevel.isShown()) { + statusBar = topLevel.getStatusBar(); + } } if (statusBar != null) { getScreen().resetClipping(); @@@ -2013,8 -2190,14 +2190,14 @@@ getScreen().unsetImageRow(mouseY); } } + + if (inScreenSelection) { + getScreen().setSelection(screenSelectionX0, screenSelectionY0, + screenSelectionX1, screenSelectionY1, screenSelectionRectangle); + } + if ((textMouse == true) && (typingHidMouse == false)) { - invertCell(mouseX, mouseY); + drawTextMouse(mouseX, mouseY); } oldDrawnMouseX = mouseX; oldDrawnMouseY = mouseY; @@@ -2154,7 -2337,7 +2337,7 @@@ * * @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 @@@ -2163,68 -2346,61 +2346,61 @@@ return; } - // Whatever window might be moving/dragging, stop it now. - for (TWindow w: windows) { - if (w.inMovements()) { - w.stopMovements(); - } + if (modalWindowActive() && !window.isModal()) { + // Do not activate a non-modal on top of a modal. + return; } - assert (windows.size() > 0); + synchronized (windows) { + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); + } + } - if (window.isHidden()) { - // Unhiding will also activate. - showWindow(window); - return; - } - assert (window.isShown()); + assert (windows.size() > 0); - if (windows.size() == 1) { - assert (window == windows.get(0)); - if (activeWindow == null) { - activeWindow = window; - window.setZ(0); - activeWindow.setActive(true); - activeWindow.onFocus(); + if (window.isHidden()) { + // Unhiding will also activate. + showWindow(window); + return; } + assert (window.isShown()); - assert (window.isActive()); - assert (activeWindow == window); - return; - } + if (windows.size() == 1) { + assert (window == windows.get(0)); + window.setZ(0); + window.setActive(true); + window.onFocus(); + return; + } - if (activeWindow == window) { - assert (window.isActive()); + if (getActiveWindow() == window) { + assert (window.isActive()); - // Window is already active, do nothing. - return; - } + // Window is already active, do nothing. + return; + } - assert (!window.isActive()); - if (activeWindow != null) { - 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; } @@@ -2242,28 -2418,39 +2418,39 @@@ 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) + } /** @@@ -2280,25 -2467,16 +2467,16 @@@ 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 */ @@@ -2316,23 -2494,16 +2494,16 @@@ 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++; @@@ -2340,22 -2511,22 +2511,22 @@@ 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(); @@@ -2373,7 -2544,8 +2544,8 @@@ synchronized (secondaryEventHandler) { secondaryEventHandler.notify(); } - } + + } // synchronized (windows) // Permit desktop to be active if it is the only thing left. if (desktop != null) { @@@ -2394,53 -2566,50 +2566,50 @@@ 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) } /** @@@ -2490,13 -2659,13 +2659,13 @@@ } 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) @@@ -2513,6 -2682,7 +2682,7 @@@ if (desktop != null) { desktop.setActive(false); } + } /** @@@ -2540,6 -2710,7 +2710,7 @@@ * @return true if the active window is overriding the menu */ private boolean overrideMenuWindowActive() { + TWindow activeWindow = getActiveWindow(); if (activeWindow != null) { if (activeWindow.hasOverriddenMenu()) { return true; @@@ -2910,7 -3081,6 +3081,6 @@@ || (mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) ) { synchronized (windows) { - Collections.sort(windows); if (windows.get(0).isModal()) { // Modal windows don't switch return; @@@ -2925,25 -3095,7 +3095,7 @@@ } 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; } } @@@ -3278,10 -3430,13 +3430,13 @@@ */ 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")); diff --combined TApplication.properties index 299c6a3,57f7c59..57f7c59 --- a/TApplication.properties +++ b/TApplication.properties @@@ -25,3 -25,6 +25,6 @@@ exitDialogText=Exit application aboutDialogTitle=About aboutDialogText=Jexer Version {0} + + searchHelpInputBoxTitle=Search Help Topics + searchHelpInputBoxCaption=Search help topics for (regex): diff --combined TEditorWidget.java index a694533,bea25ed..bea25ed --- a/TEditorWidget.java +++ b/TEditorWidget.java @@@ -29,21 -29,26 +29,26 @@@ package jexer; import java.io.IOException; + import java.util.ArrayList; + import java.util.List; import jexer.bits.CellAttributes; + import jexer.bits.StringUtils; + import jexer.event.TCommandEvent; import jexer.event.TKeypressEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; import jexer.teditor.Document; import jexer.teditor.Line; import jexer.teditor.Word; + import static jexer.TCommand.*; import static jexer.TKeypress.*; /** * TEditorWidget displays an editable text document. It is unaware of * scrolling behavior, but can respond to mouse and keyboard events. */ - public class TEditorWidget extends TWidget { + public class TEditorWidget extends TWidget implements EditMenuUser { // ------------------------------------------------------------------------ // Constants -------------------------------------------------------------- @@@ -61,10 -66,10 +66,10 @@@ /** * The document being edited. */ - private Document document; + protected Document document; /** - * The default color for the TEditor class. + * The default color for the editable text. */ private CellAttributes defaultColor = null; @@@ -78,6 -83,67 +83,67 @@@ */ private int leftColumn = 0; + /** + * If true, the mouse is dragging a selection. + */ + private boolean inSelection = false; + + /** + * Selection starting column. + */ + private int selectionColumn0; + + /** + * Selection starting line. + */ + private int selectionLine0; + + /** + * Selection ending column. + */ + private int selectionColumn1; + + /** + * Selection ending line. + */ + private int selectionLine1; + + /** + * The list of undo/redo states. + */ + private List undoList = new ArrayList(); + + /** + * The position in undoList for undo/redo. + */ + private int undoListI = 0; + + /** + * The maximum size of the undo list. + */ + private int undoLevel = 50; + + /** + * The saved state for an undo/redo operation. + */ + private class SavedState { + /** + * The Document state. + */ + public Document document; + + /** + * The topmost line number in the visible area. 0-based. + */ + public int topLine = 0; + + /** + * The leftmost column number in the visible area. 0-based. + */ + public int leftColumn = 0; + + } + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@@ -105,36 -171,9 +171,9 @@@ } // ------------------------------------------------------------------------ - // TWidget ---------------------------------------------------------------- + // Event handlers --------------------------------------------------------- // ------------------------------------------------------------------------ - /** - * Draw the text box. - */ - @Override - public void draw() { - for (int i = 0; i < getHeight(); i++) { - // Background line - getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor); - - // Now draw document's line - if (topLine + i < document.getLineCount()) { - Line line = document.getLine(topLine + i); - int x = 0; - for (Word word: line.getWords()) { - // For now, we are cheating: draw outside the left region - // if needed and let screen do the clipping. - getScreen().putStringXY(x - leftColumn, i, word.getText(), - word.getColor()); - x += word.getDisplayLength(); - if (x - leftColumn > getWidth()) { - break; - } - } - } - } - } - /** * Handle mouse press events. * @@@ -162,9 -201,23 +201,23 @@@ } if (mouse.isMouse1()) { - // Set the row and column + // Selection. int newLine = topLine + mouse.getY(); int newX = leftColumn + mouse.getX(); + + inSelection = true; + if (newLine > document.getLineCount() - 1) { + selectionLine0 = document.getLineCount() - 1; + } else { + selectionLine0 = topLine + mouse.getY(); + } + selectionColumn0 = leftColumn + mouse.getX(); + selectionColumn0 = Math.max(0, Math.min(selectionColumn0, + document.getLine(selectionLine0).getDisplayLength() - 1)); + selectionColumn1 = selectionColumn0; + selectionLine1 = selectionLine0; + + // Set the row and column if (newLine > document.getLineCount() - 1) { // Go to the end document.setLineNumber(document.getLineCount() - 1); @@@ -175,6 -228,10 +228,10 @@@ setCursorY(mouse.getY()); } alignCursor(); + if (inSelection) { + selectionColumn1 = document.getCursor(); + selectionLine1 = document.getLineNumber(); + } return; } @@@ -187,6 -244,76 +244,76 @@@ document.setCursor(newX); setCursorX(mouse.getX()); } + if (inSelection) { + selectionColumn1 = document.getCursor(); + selectionLine1 = document.getLineNumber(); + } + return; + } else { + inSelection = false; + } + + // Pass to children + super.onMouseDown(mouse); + } + + /** + * Handle mouse motion events. + * + * @param mouse mouse motion event + */ + @Override + public void onMouseMotion(final TMouseEvent mouse) { + + if (mouse.isMouse1()) { + // Set the row and column + int newLine = topLine + mouse.getY(); + int newX = leftColumn + mouse.getX(); + if ((newLine < 0) || (newX < 0)) { + return; + } + + // Selection. + if (inSelection) { + selectionColumn1 = newX; + selectionLine1 = newLine; + } else { + inSelection = true; + selectionColumn0 = newX; + selectionLine0 = newLine; + selectionColumn1 = selectionColumn0; + selectionLine1 = selectionLine0; + } + + if (newLine > document.getLineCount() - 1) { + // Go to the end + document.setLineNumber(document.getLineCount() - 1); + document.end(); + if (newLine > document.getLineCount() - 1) { + setCursorY(document.getLineCount() - 1 - topLine); + } else { + setCursorY(mouse.getY()); + } + alignCursor(); + if (inSelection) { + selectionColumn1 = document.getCursor(); + selectionLine1 = document.getLineNumber(); + } + return; + } + document.setLineNumber(newLine); + setCursorY(mouse.getY()); + if (newX >= document.getCurrentLine().getDisplayLength()) { + document.end(); + alignCursor(); + } else { + document.setCursor(newX); + setCursorX(mouse.getX()); + } + if (inSelection) { + selectionColumn1 = document.getCursor(); + selectionLine1 = document.getLineNumber(); + } return; } @@@ -201,35 -328,93 +328,93 @@@ */ @Override public void onKeypress(final TKeypressEvent keypress) { - if (keypress.equals(kbLeft)) { + if (keypress.getKey().isShift()) { + if (keypress.equals(kbShiftLeft) + || keypress.equals(kbShiftRight) + || keypress.equals(kbShiftUp) + || keypress.equals(kbShiftDown) + || keypress.equals(kbShiftPgDn) + || keypress.equals(kbShiftPgUp) + || keypress.equals(kbShiftHome) + || keypress.equals(kbShiftEnd) + ) { + // Shifted navigation keys enable selection + if (!inSelection) { + inSelection = true; + selectionColumn0 = document.getCursor(); + selectionLine0 = document.getLineNumber(); + selectionColumn1 = selectionColumn0; + selectionLine1 = selectionLine0; + } + } + } else { + if (keypress.equals(kbLeft) + || keypress.equals(kbRight) + || keypress.equals(kbUp) + || keypress.equals(kbDown) + || keypress.equals(kbPgDn) + || keypress.equals(kbPgUp) + || keypress.equals(kbHome) + || keypress.equals(kbEnd) + ) { + // Non-shifted navigation keys disable selection. + inSelection = false; + } + if ((selectionColumn0 == selectionColumn1) + && (selectionLine0 == selectionLine1) + ) { + // The user clicked a spot and started typing. + inSelection = false; + } + } + + if (keypress.equals(kbLeft) + || keypress.equals(kbShiftLeft) + ) { document.left(); alignTopLine(false); - } else if (keypress.equals(kbRight)) { + } else if (keypress.equals(kbRight) + || keypress.equals(kbShiftRight) + ) { document.right(); alignTopLine(true); } else if (keypress.equals(kbAltLeft) || keypress.equals(kbCtrlLeft) + || keypress.equals(kbAltShiftLeft) + || keypress.equals(kbCtrlShiftLeft) ) { document.backwardsWord(); alignTopLine(false); } else if (keypress.equals(kbAltRight) || keypress.equals(kbCtrlRight) + || keypress.equals(kbAltShiftRight) + || keypress.equals(kbCtrlShiftRight) ) { document.forwardsWord(); alignTopLine(true); - } else if (keypress.equals(kbUp)) { + } else if (keypress.equals(kbUp) + || keypress.equals(kbShiftUp) + ) { document.up(); alignTopLine(false); - } else if (keypress.equals(kbDown)) { + } else if (keypress.equals(kbDown) + || keypress.equals(kbShiftDown) + ) { document.down(); alignTopLine(true); - } else if (keypress.equals(kbPgUp)) { + } else if (keypress.equals(kbPgUp) + || keypress.equals(kbShiftPgUp) + ) { document.up(getHeight() - 1); alignTopLine(false); - } else if (keypress.equals(kbPgDn)) { + } else if (keypress.equals(kbPgDn) + || keypress.equals(kbShiftPgDn) + ) { document.down(getHeight() - 1); alignTopLine(true); - } else if (keypress.equals(kbHome)) { + } else if (keypress.equals(kbHome) + || keypress.equals(kbShiftHome) + ) { if (document.home()) { leftColumn = 0; if (leftColumn < 0) { @@@ -237,39 -422,62 +422,62 @@@ } setCursorX(0); } - } else if (keypress.equals(kbEnd)) { + } else if (keypress.equals(kbEnd) + || keypress.equals(kbShiftEnd) + ) { if (document.end()) { alignCursor(); } - } else if (keypress.equals(kbCtrlHome)) { + } else if (keypress.equals(kbCtrlHome) + || keypress.equals(kbCtrlShiftHome) + ) { document.setLineNumber(0); document.home(); topLine = 0; leftColumn = 0; setCursorX(0); setCursorY(0); - } else if (keypress.equals(kbCtrlEnd)) { + } else if (keypress.equals(kbCtrlEnd) + || keypress.equals(kbCtrlShiftEnd) + ) { document.setLineNumber(document.getLineCount() - 1); document.end(); alignTopLine(false); } else if (keypress.equals(kbIns)) { - document.setOverwrite(!document.getOverwrite()); + document.setOverwrite(!document.isOverwrite()); } else if (keypress.equals(kbDel)) { - document.del(); - alignCursor(); + if (inSelection) { + deleteSelection(); + alignCursor(); + } else { + saveUndo(); + document.del(); + alignCursor(); + } } else if (keypress.equals(kbBackspace) || keypress.equals(kbBackspaceDel) ) { - document.backspace(); - alignTopLine(false); - } else if (keypress.equals(kbTab)) { - // TODO: tab character. For now just add spaces until we hit - // modulo 8. - for (int i = document.getCursor(); (i + 1) % 8 != 0; i++) { - document.addChar(' '); + if (inSelection) { + deleteSelection(); + alignTopLine(false); + } else { + saveUndo(); + document.backspace(); + alignTopLine(false); } + } else if (keypress.equals(kbTab)) { + deleteSelection(); + saveUndo(); + document.tab(); + alignCursor(); + } else if (keypress.equals(kbShiftTab)) { + deleteSelection(); + saveUndo(); + document.backTab(); alignCursor(); } else if (keypress.equals(kbEnter)) { + deleteSelection(); + saveUndo(); document.enter(); alignTopLine(true); } else if (!keypress.getKey().isFnKey() @@@ -277,12 -485,19 +485,19 @@@ && !keypress.getKey().isCtrl() ) { // Plain old keystroke, process it + deleteSelection(); + saveUndo(); document.addChar(keypress.getKey().getChar()); alignCursor(); } else { // Pass other keys (tab etc.) on to TWidget super.onKeypress(keypress); } + + if (inSelection) { + selectionColumn1 = document.getCursor(); + selectionLine1 = document.getLineNumber(); + } } /** @@@ -313,10 -528,159 +528,159 @@@ } } + /** + * Handle posted command events. + * + * @param command command event + */ + @Override + public void onCommand(final TCommandEvent command) { + if (command.equals(cmCut)) { + // Copy text to clipboard, and then remove it. + copySelection(); + deleteSelection(); + return; + } + + if (command.equals(cmCopy)) { + // Copy text to clipboard. + copySelection(); + return; + } + + if (command.equals(cmPaste)) { + // Delete selected text, then paste text from clipboard. + deleteSelection(); + + String text = getClipboard().pasteText(); + if (text != null) { + for (int i = 0; i < text.length(); ) { + int ch = text.codePointAt(i); + switch (ch) { + case '\n': + onKeypress(new TKeypressEvent(kbEnter)); + break; + case '\t': + onKeypress(new TKeypressEvent(kbTab)); + break; + default: + if ((ch >= 0x20) && (ch != 0x7F)) { + onKeypress(new TKeypressEvent(false, 0, ch, + false, false, false)); + } + break; + } + + i += Character.charCount(ch); + } + } + return; + } + + if (command.equals(cmClear)) { + // Remove text. + deleteSelection(); + return; + } + + } + + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw the text box. + */ + @Override + public void draw() { + CellAttributes selectedColor = getTheme().getColor("teditor.selected"); + + boolean drawSelection = true; + + int startCol = selectionColumn0; + int startRow = selectionLine0; + int endCol = selectionColumn1; + int endRow = selectionLine1; + + if (((selectionColumn1 < selectionColumn0) + && (selectionLine1 == selectionLine0)) + || (selectionLine1 < selectionLine0) + ) { + // The user selected from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startCol = selectionColumn1; + startRow = selectionLine1; + endCol = selectionColumn0; + endRow = selectionLine0; + } + if ((startCol == endCol) && (startRow == endRow)) { + drawSelection = false; + } + + for (int i = 0; i < getHeight(); i++) { + // Background line + getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor); + + // Now draw document's line + if (topLine + i < document.getLineCount()) { + Line line = document.getLine(topLine + i); + int x = 0; + for (Word word: line.getWords()) { + // For now, we are cheating: draw outside the left region + // if needed and let screen do the clipping. + getScreen().putStringXY(x - leftColumn, i, word.getText(), + word.getColor()); + x += word.getDisplayLength(); + if (x - leftColumn > getWidth()) { + break; + } + } + + // Highlight selected region + if (inSelection && drawSelection) { + if (startRow == endRow) { + if (topLine + i == startRow) { + for (x = startCol; x <= endCol; x++) { + putAttrXY(x - leftColumn, i, selectedColor); + } + } + } else { + if (topLine + i == startRow) { + for (x = startCol; x < line.getDisplayLength(); x++) { + putAttrXY(x - leftColumn, i, selectedColor); + } + } else if (topLine + i == endRow) { + for (x = 0; x <= endCol; x++) { + putAttrXY(x - leftColumn, i, selectedColor); + } + } else if ((topLine + i >= startRow) + && (topLine + i <= endRow) + ) { + for (x = 0; x < getWidth(); x++) { + putAttrXY(x, i, selectedColor); + } + } + } + } + + } + } + } + // ------------------------------------------------------------------------ // TEditorWidget ---------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Set the undo level. + * + * @param undoLevel the maximum number of undo operations + */ + public void setUndoLevel(final int undoLevel) { + this.undoLevel = undoLevel; + } + /** * Align visible area with document current line. * @@@ -524,6 -888,17 +888,17 @@@ return document.getLineLengthMax() + 1; } + /** + * Get the current editing row plain text. 1-based. + * + * @param row the editing row number. Row 1 is the first row. + * @return the plain text of the row + */ + public String getEditingRawLine(final int row) { + Line line = document.getLine(row - 1); + return line.getRawString(); + } + /** * Get the dirty value. * @@@ -533,6 -908,22 +908,22 @@@ return document.isDirty(); } + /** + * Unset the dirty flag. + */ + public void setNotDirty() { + document.setNotDirty(); + } + + /** + * Get the overwrite value. + * + * @return true if new text will overwrite old text + */ + public boolean isOverwrite() { + return document.isOverwrite(); + } + /** * Save contents to file. * @@@ -543,4 -934,508 +934,508 @@@ document.saveToFilename(filename); } + /** + * Delete text within the selection bounds. + */ + private void deleteSelection() { + if (!inSelection) { + return; + } + + saveUndo(); + + inSelection = false; + + int startCol = selectionColumn0; + int startRow = selectionLine0; + int endCol = selectionColumn1; + int endRow = selectionLine1; + + /* + System.err.println("INITIAL: " + startRow + " " + startCol + " " + + endRow + " " + endCol + " " + + document.getLineNumber() + " " + document.getCursor()); + */ + + if (((selectionColumn1 < selectionColumn0) + && (selectionLine1 == selectionLine0)) + || (selectionLine1 < selectionLine0) + ) { + // The user selected from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startCol = selectionColumn1; + startRow = selectionLine1; + endCol = selectionColumn0; + endRow = selectionLine0; + + if (endRow >= document.getLineCount()) { + // The selection started beyond EOF, trim it to EOF. + endRow = document.getLineCount() - 1; + endCol = document.getLine(endRow).getDisplayLength(); + } else if (endRow == document.getLineCount() - 1) { + // The selection started beyond EOF, trim it to EOF. + if (endCol >= document.getLine(endRow).getDisplayLength()) { + endCol = document.getLine(endRow).getDisplayLength() - 1; + } + } + } + /* + System.err.println("FLIP: " + startRow + " " + startCol + " " + + endRow + " " + endCol + " " + + document.getLineNumber() + " " + document.getCursor()); + System.err.println(" --END: " + endRow + " " + document.getLineCount() + + " " + document.getLine(endRow).getDisplayLength()); + */ + + assert (endRow < document.getLineCount()); + if (endCol >= document.getLine(endRow).getDisplayLength()) { + endCol = document.getLine(endRow).getDisplayLength() - 1; + } + if (endCol < 0) { + endCol = 0; + } + if (startCol >= document.getLine(startRow).getDisplayLength()) { + startCol = document.getLine(startRow).getDisplayLength() - 1; + } + if (startCol < 0) { + startCol = 0; + } + + // Place the cursor on the selection end, and "press backspace" until + // the cursor matches the selection start. + /* + System.err.println("BEFORE: " + startRow + " " + startCol + " " + + endRow + " " + endCol + " " + + document.getLineNumber() + " " + document.getCursor()); + */ + document.setLineNumber(endRow); + document.setCursor(endCol + 1); + while (!((document.getLineNumber() == startRow) + && (document.getCursor() == startCol)) + ) { + /* + System.err.println("DURING: " + startRow + " " + startCol + " " + + endRow + " " + endCol + " " + + document.getLineNumber() + " " + document.getCursor()); + */ + + document.backspace(); + } + alignTopLine(true); + } + + /** + * Copy text within the selection bounds to clipboard. + */ + private void copySelection() { + if (!inSelection) { + return; + } + getClipboard().copyText(getSelection()); + } + + /** + * Set the selection. + * + * @param startRow the starting row number. 0-based: row 0 is the first + * row. + * @param startColumn the starting column number. 0-based: column 0 is + * the first column. + * @param endRow the ending row number. 0-based: row 0 is the first row. + * @param endColumn the ending column number. 0-based: column 0 is the + * first column. + */ + public void setSelection(final int startRow, final int startColumn, + final int endRow, final int endColumn) { + + inSelection = true; + selectionLine0 = startRow; + selectionColumn0 = startColumn; + selectionLine1 = endRow; + selectionColumn1 = endColumn; + } + + /** + * Copy text within the selection bounds to a string. + * + * @return the selection as a string, or null if there is no selection + */ + public String getSelection() { + if (!inSelection) { + return null; + } + + int startCol = selectionColumn0; + int startRow = selectionLine0; + int endCol = selectionColumn1; + int endRow = selectionLine1; + + if (((selectionColumn1 < selectionColumn0) + && (selectionLine1 == selectionLine0)) + || (selectionLine1 < selectionLine0) + ) { + // The user selected from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startCol = selectionColumn1; + startRow = selectionLine1; + endCol = selectionColumn0; + endRow = selectionLine0; + } + + StringBuilder sb = new StringBuilder(); + + if (endRow > startRow) { + // First line + String line = document.getLine(startRow).getRawString(); + int x = 0; + for (int i = 0; i < line.length(); ) { + int ch = line.codePointAt(i); + + if (x >= startCol) { + sb.append(Character.toChars(ch)); + } + x += StringUtils.width(ch); + i += Character.charCount(ch); + } + sb.append("\n"); + + // Middle lines + for (int y = startRow + 1; y < endRow; y++) { + sb.append(document.getLine(y).getRawString()); + sb.append("\n"); + } + + // Final line + line = document.getLine(endRow).getRawString(); + x = 0; + for (int i = 0; i < line.length(); ) { + int ch = line.codePointAt(i); + + if (x > endCol) { + break; + } + + sb.append(Character.toChars(ch)); + x += StringUtils.width(ch); + i += Character.charCount(ch); + } + } else { + assert (startRow == endRow); + + // Only one line + String line = document.getLine(startRow).getRawString(); + int x = 0; + for (int i = 0; i < line.length(); ) { + int ch = line.codePointAt(i); + + if ((x >= startCol) && (x <= endCol)) { + sb.append(Character.toChars(ch)); + } + + x += StringUtils.width(ch); + i += Character.charCount(ch); + } + } + return sb.toString(); + } + + /** + * Get the selection starting row number. + * + * @return the starting row number, or -1 if there is no selection. + * 0-based: row 0 is the first row. + */ + public int getSelectionStartRow() { + if (!inSelection) { + return -1; + } + + int startCol = selectionColumn0; + int startRow = selectionLine0; + int endCol = selectionColumn1; + int endRow = selectionLine1; + + if (((selectionColumn1 < selectionColumn0) + && (selectionLine1 == selectionLine0)) + || (selectionLine1 < selectionLine0) + ) { + // The user selected from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startCol = selectionColumn1; + startRow = selectionLine1; + endCol = selectionColumn0; + endRow = selectionLine0; + } + return startRow; + } + + /** + * Get the selection starting column number. + * + * @return the starting column number, or -1 if there is no selection. + * 0-based: column 0 is the first column. + */ + public int getSelectionStartColumn() { + if (!inSelection) { + return -1; + } + + int startCol = selectionColumn0; + int startRow = selectionLine0; + int endCol = selectionColumn1; + int endRow = selectionLine1; + + if (((selectionColumn1 < selectionColumn0) + && (selectionLine1 == selectionLine0)) + || (selectionLine1 < selectionLine0) + ) { + // The user selected from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startCol = selectionColumn1; + startRow = selectionLine1; + endCol = selectionColumn0; + endRow = selectionLine0; + } + return startCol; + } + + /** + * Get the selection ending row number. + * + * @return the ending row number, or -1 if there is no selection. + * 0-based: row 0 is the first row. + */ + public int getSelectionEndRow() { + if (!inSelection) { + return -1; + } + + int startCol = selectionColumn0; + int startRow = selectionLine0; + int endCol = selectionColumn1; + int endRow = selectionLine1; + + if (((selectionColumn1 < selectionColumn0) + && (selectionLine1 == selectionLine0)) + || (selectionLine1 < selectionLine0) + ) { + // The user selected from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startCol = selectionColumn1; + startRow = selectionLine1; + endCol = selectionColumn0; + endRow = selectionLine0; + } + return endRow; + } + + /** + * Get the selection ending column number. + * + * @return the ending column number, or -1 if there is no selection. + * 0-based: column 0 is the first column. + */ + public int getSelectionEndColumn() { + if (!inSelection) { + return -1; + } + + int startCol = selectionColumn0; + int startRow = selectionLine0; + int endCol = selectionColumn1; + int endRow = selectionLine1; + + if (((selectionColumn1 < selectionColumn0) + && (selectionLine1 == selectionLine0)) + || (selectionLine1 < selectionLine0) + ) { + // The user selected from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startCol = selectionColumn1; + startRow = selectionLine1; + endCol = selectionColumn0; + endRow = selectionLine0; + } + return endCol; + } + + /** + * Unset the selection. + */ + public void unsetSelection() { + inSelection = false; + } + + /** + * Replace whatever is being selected with new text. If not in + * selection, nothing is replaced. + * + * @param text the new replacement text + */ + public void replaceSelection(final String text) { + if (!inSelection) { + return; + } + + // Delete selected text, then paste text from clipboard. + deleteSelection(); + + for (int i = 0; i < text.length(); ) { + int ch = text.codePointAt(i); + switch (ch) { + case '\n': + onKeypress(new TKeypressEvent(kbEnter)); + break; + case '\t': + onKeypress(new TKeypressEvent(kbTab)); + break; + default: + if ((ch >= 0x20) && (ch != 0x7F)) { + onKeypress(new TKeypressEvent(false, 0, ch, + false, false, false)); + } + break; + } + i += Character.charCount(ch); + } + } + + /** + * Check if selection is available. + * + * @return true if a selection has been made + */ + public boolean hasSelection() { + return inSelection; + } + + /** + * Get the entire contents of the editor as one string. + * + * @return the editor contents + */ + public String getText() { + return document.getText(); + } + + /** + * Set the entire contents of the editor from one string. + * + * @param text the new contents + */ + public void setText(final String text) { + document = new Document(text, defaultColor); + unsetSelection(); + topLine = 0; + leftColumn = 0; + } + + // ------------------------------------------------------------------------ + // EditMenuUser ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Check if the cut menu item should be enabled. + * + * @return true if the cut menu item should be enabled + */ + public boolean isEditMenuCut() { + return true; + } + + /** + * Check if the copy menu item should be enabled. + * + * @return true if the copy menu item should be enabled + */ + public boolean isEditMenuCopy() { + return true; + } + + /** + * Check if the paste menu item should be enabled. + * + * @return true if the paste menu item should be enabled + */ + public boolean isEditMenuPaste() { + return true; + } + + /** + * Check if the clear menu item should be enabled. + * + * @return true if the clear menu item should be enabled + */ + public boolean isEditMenuClear() { + return true; + } + + /** + * Save undo state. + */ + private void saveUndo() { + SavedState state = new SavedState(); + state.document = document.dup(); + state.topLine = topLine; + state.leftColumn = leftColumn; + if (undoLevel > 0) { + while (undoList.size() > undoLevel) { + undoList.remove(0); + } + } + undoList.add(state); + undoListI = undoList.size() - 1; + } + + /** + * Undo an edit. + */ + public void undo() { + inSelection = false; + if ((undoListI >= 0) && (undoListI < undoList.size())) { + SavedState state = undoList.get(undoListI); + document = state.document.dup(); + topLine = state.topLine; + leftColumn = state.leftColumn; + undoListI--; + setCursorY(document.getLineNumber() - topLine); + alignCursor(); + } + } + + /** + * Redo an edit. + */ + public void redo() { + inSelection = false; + if ((undoListI >= 0) && (undoListI < undoList.size())) { + SavedState state = undoList.get(undoListI); + document = state.document.dup(); + topLine = state.topLine; + leftColumn = state.leftColumn; + undoListI++; + setCursorY(document.getLineNumber() - topLine); + alignCursor(); + } + } + + /** + * Trim trailing whitespace from lines and trailing empty + * lines from the document. + */ + public void cleanWhitespace() { + document.cleanWhitespace(); + setCursorY(document.getLineNumber() - topLine); + alignCursor(); + } + + /** + * Set keyword highlighting. + * + * @param enabled if true, enable keyword highlighting + */ + public void setHighlighting(final boolean enabled) { + document.setHighlighting(enabled); + } + } diff --combined TEditorWindow.java index d78185c,a28376b..a28376b --- a/TEditorWindow.java +++ b/TEditorWindow.java @@@ -44,8 -44,10 +44,10 @@@ import jexer.bits.CellAttributes import jexer.bits.GraphicsChars; import jexer.event.TCommandEvent; import jexer.event.TKeypressEvent; + import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; + import jexer.menu.TMenu; import static jexer.TCommand.*; import static jexer.TKeypress.*; @@@ -150,28 -152,27 +152,27 @@@ public class TEditorWindow extends TScr } // ------------------------------------------------------------------------ - // TWindow ---------------------------------------------------------------- + // Event handlers --------------------------------------------------------- // ------------------------------------------------------------------------ /** - * Draw the window. + * Called by application.switchWindow() when this window gets the + * focus, and also by application.addWindow(). */ - @Override - public void draw() { - // Draw as normal. - super.draw(); - - // Add the row:col on the bottom row - CellAttributes borderColor = getBorder(); - String location = String.format(" %d:%d ", - editField.getEditingRowNumber(), - editField.getEditingColumnNumber()); - int colon = location.indexOf(':'); - putStringXY(10 - colon, getHeight() - 1, location, borderColor); + public void onFocus() { + super.onFocus(); + getApplication().enableMenuItem(TMenu.MID_UNDO); + getApplication().enableMenuItem(TMenu.MID_REDO); + } - if (editField.isDirty()) { - putCharXY(2, getHeight() - 1, GraphicsChars.OCTOSTAR, borderColor); - } + /** + * Called by application.switchWindow() when another window gets the + * focus. + */ + public void onUnfocus() { + super.onUnfocus(); + getApplication().disableMenuItem(TMenu.MID_UNDO); + getApplication().disableMenuItem(TMenu.MID_REDO); } /** @@@ -351,6 -352,48 +352,48 @@@ super.onCommand(command); } + /** + * Handle posted menu events. + * + * @param menu menu event + */ + @Override + public void onMenu(final TMenuEvent menu) { + switch (menu.getId()) { + case TMenu.MID_UNDO: + editField.undo(); + break; + case TMenu.MID_REDO: + editField.redo(); + break; + } + } + + // ------------------------------------------------------------------------ + // TWindow ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw the window. + */ + @Override + public void draw() { + // Draw as normal. + super.draw(); + + // Add the row:col on the bottom row + CellAttributes borderColor = getBorder(); + String location = String.format(" %d:%d ", + editField.getEditingRowNumber(), + editField.getEditingColumnNumber()); + int colon = location.indexOf(':'); + putStringXY(10 - colon, getHeight() - 1, location, borderColor); + + if (editField.isDirty()) { + putCharXY(2, getHeight() - 1, GraphicsChars.OCTOSTAR, borderColor); + } + } + /** * Returns true if this window does not want the application-wide mouse * cursor drawn over it. diff --combined TExceptionDialog.java index 227aceb,f526a64..f526a64 --- a/TExceptionDialog.java +++ b/TExceptionDialog.java @@@ -82,7 -82,7 +82,7 @@@ public class TExceptionDialog extends T final Throwable exception) { super(application, i18n.getString("windowTitle"), - 1, 1, 70, 20, CENTERED | MODAL); + 1, 1, 78, 22, CENTERED | MODAL); this.exception = exception; @@@ -100,14 -100,15 +100,15 @@@ 2, 6, "ttext", false); ArrayList stackTraceStrings = new ArrayList(); + stackTraceStrings.add(exception.getMessage()); StackTraceElement [] stack = exception.getStackTrace(); for (int i = 0; i < stack.length; i++) { stackTraceStrings.add(stack[i].toString()); } - stackTrace = addList(stackTraceStrings, 2, 7, getWidth() - 6, 8); + stackTrace = addList(stackTraceStrings, 2, 7, getWidth() - 6, 10); // Buttons - addButton(i18n.getString("saveButton"), 19, getHeight() - 4, + addButton(i18n.getString("saveButton"), 21, getHeight() - 4, new TAction() { public void DO() { saveToFile(); @@@ -115,7 -116,7 +116,7 @@@ }); TButton closeButton = addButton(i18n.getString("closeButton"), - 35, getHeight() - 4, + 37, getHeight() - 4, new TAction() { public void DO() { // Don't do anything, just close the window. diff --combined TExceptionDialog.properties index d07998c,9e5857a..9e5857a --- a/TExceptionDialog.properties +++ b/TExceptionDialog.properties @@@ -1,10 -1,10 +1,10 @@@ windowTitle=Java Exception Caught statusBar=Exception - captionLine1=An error has occurred. This may be due to a programming bug, but - captionLine2=could also be a correctable or temporary issue. The stack trace - captionLine3=is reported below. If you wish to submit a bug report, please - captionLine4=use the Save button to create a more detailed error log. + captionLine1=An error has occurred. This may be due to a programming bug, but could + captionLine2=also be a correctable or temporary issue. The stack trace is reported + captionLine3=below. If you wish to submit a bug report, please use the Save button + captionLine4=to create a more detailed error log. exceptionString={0}: {1} diff --combined TField.java index 7c8b5bc,90dd4e4..90dd4e4 --- a/TField.java +++ b/TField.java @@@ -31,14 -31,16 +31,16 @@@ package jexer import jexer.bits.CellAttributes; import jexer.bits.GraphicsChars; import jexer.bits.StringUtils; + import jexer.event.TCommandEvent; import jexer.event.TKeypressEvent; import jexer.event.TMouseEvent; + import static jexer.TCommand.*; import static jexer.TKeypress.*; /** * TField implements an editable text field. */ - public class TField extends TWidget { + public class TField extends TWidget implements EditMenuUser { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- @@@ -234,12 -236,13 +236,13 @@@ if (keypress.equals(kbRight)) { if (position < text.length()) { + int lastPosition = position; screenPosition += StringUtils.width(text.codePointAt(position)); position += Character.charCount(text.codePointAt(position)); if (fixed == true) { if (screenPosition == getWidth()) { screenPosition--; - position -= Character.charCount(text.codePointAt(position)); + position -= Character.charCount(text.codePointAt(lastPosition)); } } else { while ((screenPosition - windowStart + @@@ -374,6 -377,43 +377,43 @@@ super.onKeypress(keypress); } + /** + * Handle posted command events. + * + * @param command command event + */ + @Override + public void onCommand(final TCommandEvent command) { + if (command.equals(cmCut)) { + // Copy text to clipboard, and then remove it. + getClipboard().copyText(text); + setText(""); + return; + } + + if (command.equals(cmCopy)) { + // Copy text to clipboard. + getClipboard().copyText(text); + return; + } + + if (command.equals(cmPaste)) { + // Paste text from clipboard. + String newText = getClipboard().pasteText(); + if (newText != null) { + setText(newText); + } + return; + } + + if (command.equals(cmClear)) { + // Remove text. + setText(""); + return; + } + + } + // ------------------------------------------------------------------------ // TWidget ---------------------------------------------------------------- // ------------------------------------------------------------------------ @@@ -467,7 -507,11 +507,11 @@@ assert (text != null); this.text = text; position = 0; + screenPosition = 0; windowStart = 0; + if ((fixed == true) && (this.text.length() > getWidth())) { + this.text = this.text.substring(0, getWidth()); + } } /** @@@ -668,4 -712,44 +712,44 @@@ updateAction = action; } + // ------------------------------------------------------------------------ + // EditMenuUser ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Check if the cut menu item should be enabled. + * + * @return true if the cut menu item should be enabled + */ + public boolean isEditMenuCut() { + return true; + } + + /** + * Check if the copy menu item should be enabled. + * + * @return true if the copy menu item should be enabled + */ + public boolean isEditMenuCopy() { + return true; + } + + /** + * Check if the paste menu item should be enabled. + * + * @return true if the paste menu item should be enabled + */ + public boolean isEditMenuPaste() { + return true; + } + + /** + * Check if the clear menu item should be enabled. + * + * @return true if the clear menu item should be enabled + */ + public boolean isEditMenuClear() { + return true; + } + } diff --combined TImage.java index cd0ce96,b7bfbd0..b7bfbd0 --- a/TImage.java +++ b/TImage.java @@@ -30,19 -30,18 +30,18 @@@ package jexer import java.awt.image.BufferedImage; - import jexer.backend.ECMA48Terminal; - import jexer.backend.MultiScreen; - import jexer.backend.SwingTerminal; import jexer.bits.Cell; + import jexer.event.TCommandEvent; import jexer.event.TKeypressEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; + import static jexer.TCommand.*; import static jexer.TKeypress.*; /** * TImage renders a piece of a bitmap image on screen. */ - public class TImage extends TWidget { + public class TImage extends TWidget implements EditMenuUser { // ------------------------------------------------------------------------ // Constants -------------------------------------------------------------- @@@ -325,6 -324,20 +324,20 @@@ resized = true; } + /** + * Handle posted command events. + * + * @param command command event + */ + @Override + public void onCommand(final TCommandEvent command) { + if (command.equals(cmCopy)) { + // Copy image to clipboard. + getClipboard().copyImage(image); + return; + } + } + // ------------------------------------------------------------------------ // TWidget ---------------------------------------------------------------- // ------------------------------------------------------------------------ @@@ -409,8 -422,21 +422,21 @@@ } Cell cell = new Cell(); - cell.setImage(image.getSubimage(x * textWidth, - y * textHeight, width, height)); + if ((width != textWidth) || (height != textHeight)) { + BufferedImage newImage; + newImage = new BufferedImage(textWidth, textHeight, + BufferedImage.TYPE_INT_ARGB); + + java.awt.Graphics gr = newImage.getGraphics(); + gr.drawImage(image.getSubimage(x * textWidth, + y * textHeight, width, height), + 0, 0, null, null); + gr.dispose(); + cell.setImage(newImage); + } else { + cell.setImage(image.getSubimage(x * textWidth, + y * textHeight, width, height)); + } cells[x][y] = cell; } @@@ -762,4 -788,44 +788,44 @@@ return newImage; } + // ------------------------------------------------------------------------ + // EditMenuUser ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Check if the cut menu item should be enabled. + * + * @return true if the cut menu item should be enabled + */ + public boolean isEditMenuCut() { + return false; + } + + /** + * Check if the copy menu item should be enabled. + * + * @return true if the copy menu item should be enabled + */ + public boolean isEditMenuCopy() { + return true; + } + + /** + * Check if the paste menu item should be enabled. + * + * @return true if the paste menu item should be enabled + */ + public boolean isEditMenuPaste() { + return false; + } + + /** + * Check if the clear menu item should be enabled. + * + * @return true if the clear menu item should be enabled + */ + public boolean isEditMenuClear() { + return false; + } + } diff --combined TKeypress.java index c965e7d,20db8bb..20db8bb --- a/TKeypress.java +++ b/TKeypress.java @@@ -612,6 -612,41 +612,41 @@@ public class TKeypress public static final TKeypress kbAltShiftZ = new TKeypress(false, 0, 'Z', true, false, true); + public static final TKeypress kbAltShiftHome = new TKeypress(true, + TKeypress.HOME, ' ', true, false, true); + public static final TKeypress kbAltShiftEnd = new TKeypress(true, + TKeypress.END, ' ', true, false, true); + public static final TKeypress kbAltShiftPgUp = new TKeypress(true, + TKeypress.PGUP, ' ', true, false, true); + public static final TKeypress kbAltShiftPgDn = new TKeypress(true, + TKeypress.PGDN, ' ', true, false, true); + public static final TKeypress kbAltShiftUp = new TKeypress(true, + TKeypress.UP, ' ', true, false, true); + public static final TKeypress kbAltShiftDown = new TKeypress(true, + TKeypress.DOWN, ' ', true, false, true); + public static final TKeypress kbAltShiftLeft = new TKeypress(true, + TKeypress.LEFT, ' ', true, false, true); + public static final TKeypress kbAltShiftRight = new TKeypress(true, + TKeypress.RIGHT, ' ', true, false, true); + + public static final TKeypress kbCtrlShiftHome = new TKeypress(true, + TKeypress.HOME, ' ', false, true, true); + public static final TKeypress kbCtrlShiftEnd = new TKeypress(true, + TKeypress.END, ' ', false, true, true); + public static final TKeypress kbCtrlShiftPgUp = new TKeypress(true, + TKeypress.PGUP, ' ', false, true, true); + public static final TKeypress kbCtrlShiftPgDn = new TKeypress(true, + TKeypress.PGDN, ' ', false, true, true); + public static final TKeypress kbCtrlShiftUp = new TKeypress(true, + TKeypress.UP, ' ', false, true, true); + public static final TKeypress kbCtrlShiftDown = new TKeypress(true, + TKeypress.DOWN, ' ', false, true, true); + public static final TKeypress kbCtrlShiftLeft = new TKeypress(true, + TKeypress.LEFT, ' ', false, true, true); + public static final TKeypress kbCtrlShiftRight = new TKeypress(true, + TKeypress.RIGHT, ' ', false, true, true); + + /** * Backspace as ^H. */ @@@ -821,6 -856,11 +856,11 @@@ return "\u25C0\u2500\u2518"; } + // Special case: Space is "Space" + if (equals(kbSpace)) { + return "Space"; + } + if (equals(kbShiftLeft)) { return "Shift+\u2190"; } diff --combined TList.java index 38a994c,12e0b8a..12e0b8a --- a/TList.java +++ b/TList.java @@@ -335,21 -335,28 +335,28 @@@ public class TList extends TScrollableW @Override public void setWidth(final int width) { super.setWidth(width); - hScroller.setWidth(getWidth() - 1); - vScroller.setX(getWidth() - 1); + if (hScroller != null) { + hScroller.setWidth(getWidth() - 1); + } + if (vScroller != null) { + vScroller.setX(getWidth() - 1); + } } /** * Override TWidget's height: we need to set child widget heights. - * time. * * @param height new widget height */ @Override public void setHeight(final int height) { super.setHeight(height); - hScroller.setY(getHeight() - 1); - vScroller.setHeight(getHeight() - 1); + if (hScroller != null) { + hScroller.setY(getHeight() - 1); + } + if (vScroller != null) { + vScroller.setHeight(getHeight() - 1); + } } /** @@@ -391,6 -398,9 +398,9 @@@ int topY = 0; for (int i = begin; i < strings.size(); i++) { String line = strings.get(i); + if (line == null) { + line = ""; + } if (getHorizontalValue() < line.length()) { line = line.substring(getHorizontalValue()); } else { diff --combined TPasswordField.java index 9c200d7,0be2b98..0be2b98 --- a/TPasswordField.java +++ b/TPasswordField.java @@@ -29,7 -29,6 +29,6 @@@ package jexer; import jexer.bits.CellAttributes; - import jexer.bits.GraphicsChars; import jexer.bits.StringUtils; /** diff --combined TRadioButton.java index 60a6288,dcc5c13..dcc5c13 --- a/TRadioButton.java +++ b/TRadioButton.java @@@ -50,9 -50,9 +50,9 @@@ public class TRadioButton extends TWidg // ------------------------------------------------------------------------ /** - * RadioButton state, true means selected. + * RadioButton state, true means selected. Note package private access. */ - private boolean selected = false; + boolean selected = false; /** * The shortcut and radio button label. @@@ -61,16 -61,16 +61,16 @@@ /** * ID for this radio button. Buttons start counting at 1 in the - * RadioGroup. + * RadioGroup. Note package private access. */ - private int id; + int id; // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ /** - * Public constructor. + * Package private constructor. * * @param parent parent widget * @param x column relative to parent @@@ -78,7 -78,7 +78,7 @@@ * @param label label to display next to (right of) the radiobutton * @param id ID for this radio button */ - public TRadioButton(final TRadioGroup parent, final int x, final int y, + TRadioButton(final TRadioGroup parent, final int x, final int y, final String label, final int id) { // Set parent and window @@@ -89,6 -89,8 +89,8 @@@ setCursorVisible(true); setCursorX(1); + + parent.addRadioButton(this); } // ------------------------------------------------------------------------ @@@ -120,8 -122,7 +122,7 @@@ public void onMouseDown(final TMouseEvent mouse) { if ((mouseOnRadioButton(mouse)) && (mouse.isMouse1())) { // Switch state - selected = true; - ((TRadioGroup) getParent()).setSelected(this); + ((TRadioGroup) getParent()).setSelected(id); } } @@@ -134,8 -135,7 +135,7 @@@ public void onKeypress(final TKeypressEvent keypress) { if (keypress.equals(kbSpace)) { - selected = true; - ((TRadioGroup) getParent()).setSelected(this); + ((TRadioGroup) getParent()).setSelected(id); return; } @@@ -222,14 -222,17 +222,17 @@@ } /** - * Set RadioButton state, true means selected. Note package private - * access. + * Set RadioButton state, true means selected. * * @param selected if true then this is the one button in the group that * is selected */ - void setSelected(final boolean selected) { - this.selected = selected; + public void setSelected(final boolean selected) { + if (selected == true) { + ((TRadioGroup) getParent()).setSelected(id); + } else { + ((TRadioGroup) getParent()).setSelected(0); + } } /** diff --combined TRadioGroup.java index a82b074,d6bd7ff..d6bd7ff --- a/TRadioGroup.java +++ b/TRadioGroup.java @@@ -54,12 -54,30 +54,30 @@@ public class TRadioGroup extends TWidge * If true, one of the children MUST be selected. Note package private * access. */ - boolean requiresSelection = true; + boolean requiresSelection = false; // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Public constructor. + * + * @param parent parent widget + * @param x column relative to parent + * @param y row relative to parent + * @param width width of group + * @param label label to display on the group box + */ + public TRadioGroup(final TWidget parent, final int x, final int y, + final int width, final String label) { + + // Set parent and window + super(parent, x, y, width, 2); + + this.label = label; + } + /** * Public constructor. * @@@ -138,19 -156,6 +156,6 @@@ return selectedButton.getId(); } - /** - * Set the new selected radio button. Note package private access. - * - * @param button new button that became selected - */ - void setSelected(final TRadioButton button) { - assert (button.isSelected()); - if ((selectedButton != null) && (selectedButton != button)) { - selectedButton.setSelected(false); - } - selectedButton = button; - } - /** * Set the new selected radio button. 1-based. * @@@ -161,19 -166,43 +166,43 @@@ return; } + for (TWidget widget: getChildren()) { + ((TRadioButton) widget).selected = false; + } if (id == 0) { - for (TWidget widget: getChildren()) { - ((TRadioButton) widget).setSelected(false); - } selectedButton = null; return; } assert ((id > 0) && (id <= getChildren().size())); TRadioButton button = (TRadioButton) (getChildren().get(id - 1)); - button.setSelected(true); + button.selected = true; selectedButton = button; } + /** + * Get the radio button that was selected. + * + * @return the selected button, or null if no button is selected + */ + public TRadioButton getSelectedButton() { + return selectedButton; + } + + /** + * Convenience function to add a radio button to this group. + * + * @param label label to display next to (right of) the radiobutton + * @param selected if true, this will be the selected radiobutton + * @return the new radio button + */ + public TRadioButton addRadioButton(final String label, + final boolean selected) { + + TRadioButton button = addRadioButton(label); + setSelected(button.id); + return button; + } + /** * Convenience function to add a radio button to this group. * @@@ -181,14 -210,25 +210,25 @@@ * @return the new radio button */ public TRadioButton addRadioButton(final String label) { - int buttonX = 1; - int buttonY = getChildren().size() + 1; + return new TRadioButton(this, 0, 0, label, 0); + } + + /** + * Package private method for RadioButton to add itself to a RadioGroup + * container. + * + * @param button the button to add + */ + void addRadioButton(final TRadioButton button) { + super.setHeight(getChildren().size() + 2); + button.setX(1); + button.setY(getChildren().size()); + button.id = getChildren().size(); + String label = button.getMnemonic().getRawLabel(); + if (StringUtils.width(label) + 4 > getWidth()) { super.setWidth(StringUtils.width(label) + 7); } - super.setHeight(getChildren().size() + 3); - TRadioButton button = new TRadioButton(this, buttonX, buttonY, label, - getChildren().size() + 1); if (getParent().getLayoutManager() != null) { getParent().getLayoutManager().resetSize(this); @@@ -196,8 -236,31 +236,31 @@@ // Default to the first item on the list. activate(getChildren().get(0)); + } - return button; + /** + * Get the requires selection flag. + * + * @return true if this radiogroup requires that one of the buttons be + * selected + */ + public boolean getRequiresSelection() { + return requiresSelection; + } + + /** + * Set the requires selection flag. + * + * @param requiresSelection if true, then this radiogroup requires that + * one of the buttons be selected + */ + public void setRequiresSelection(final boolean requiresSelection) { + this.requiresSelection = requiresSelection; + if (requiresSelection) { + if ((getChildren().size() > 0) && (selectedButton == null)) { + setSelected(1); + } + } } } diff --combined TSplitPane.java index 7c85278,b308e9b..b308e9b --- a/TSplitPane.java +++ b/TSplitPane.java @@@ -30,10 -30,8 +30,8 @@@ package jexer import jexer.bits.CellAttributes; import jexer.bits.GraphicsChars; - import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; - import jexer.menu.TMenu; /** * TSplitPane contains two widgets with a draggable horizontal or vertical @@@ -225,7 -223,28 +223,28 @@@ public class TSplitPane extends TWidge CellAttributes attr = getTheme().getColor("tsplitpane"); if (vertical) { vLineXY(split, 0, getHeight(), GraphicsChars.WINDOW_SIDE, attr); - // TODO: draw intersections of children + + // Draw intersections of children + if ((left instanceof TSplitPane) + && (((TSplitPane) left).vertical == false) + && (right instanceof TSplitPane) + && (((TSplitPane) right).vertical == false) + && (((TSplitPane) left).split == ((TSplitPane) right).split) + ) { + putCharXY(split, ((TSplitPane) left).split, '\u253C', attr); + } else { + if ((left instanceof TSplitPane) + && (((TSplitPane) left).vertical == false) + ) { + putCharXY(split, ((TSplitPane) left).split, '\u2524', attr); + } + if ((right instanceof TSplitPane) + && (((TSplitPane) right).vertical == false) + ) { + putCharXY(split, ((TSplitPane) right).split, '\u251C', + attr); + } + } if ((mouse != null) && (mouse.getAbsoluteX() == getAbsoluteX() + split) @@@ -237,7 -256,28 +256,28 @@@ } } else { hLineXY(0, split, getWidth(), GraphicsChars.SINGLE_BAR, attr); - // TODO: draw intersections of children + + // Draw intersections of children + if ((top instanceof TSplitPane) + && (((TSplitPane) top).vertical == true) + && (bottom instanceof TSplitPane) + && (((TSplitPane) bottom).vertical == true) + && (((TSplitPane) top).split == ((TSplitPane) bottom).split) + ) { + putCharXY(((TSplitPane) top).split, split, '\u253C', attr); + } else { + if ((top instanceof TSplitPane) + && (((TSplitPane) top).vertical == true) + ) { + putCharXY(((TSplitPane) top).split, split, '\u2534', attr); + } + if ((bottom instanceof TSplitPane) + && (((TSplitPane) bottom).vertical == true) + ) { + putCharXY(((TSplitPane) bottom).split, split, '\u252C', + attr); + } + } if ((mouse != null) && (mouse.getAbsoluteY() == getAbsoluteY() + split) @@@ -595,7 -635,7 +635,7 @@@ keep.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(), getHeight())); } - + return keep; } diff --combined TTableWidget.java index 9b4d7c9,749b731..749b731 --- a/TTableWidget.java +++ b/TTableWidget.java @@@ -1426,8 -1426,6 +1426,6 @@@ public class TTableWidget extends TWidg for (int i = 0; i < list.size(); i++) { rows.get(selectedRow).get(i).setText(list.get(i)); } - - // TODO: detect header line } } finally { if (reader != null) { diff --combined TTableWindow.java index 44ff7b4,766ceaf..766ceaf --- a/TTableWindow.java +++ b/TTableWindow.java @@@ -112,7 -112,6 +112,6 @@@ public class TTableWindow extends TScro */ public void onFocus() { // Enable the table menu items. - getApplication().enableMenuItem(TMenu.MID_CUT); getApplication().enableMenuItem(TMenu.MID_TABLE_RENAME_COLUMN); getApplication().enableMenuItem(TMenu.MID_TABLE_RENAME_ROW); getApplication().enableMenuItem(TMenu.MID_TABLE_VIEW_ROW_LABELS); @@@ -171,7 -170,6 +170,6 @@@ */ public void onUnfocus() { // Disable the table menu items. - getApplication().disableMenuItem(TMenu.MID_CUT); getApplication().disableMenuItem(TMenu.MID_TABLE_RENAME_COLUMN); getApplication().disableMenuItem(TMenu.MID_TABLE_RENAME_ROW); getApplication().disableMenuItem(TMenu.MID_TABLE_VIEW_ROW_LABELS); diff --combined TTerminalWidget.java index a269609,bf51e6b..bf51e6b --- a/TTerminalWidget.java +++ b/TTerminalWidget.java @@@ -28,27 -28,21 +28,21 @@@ */ package jexer; - import java.awt.Font; - import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.image.BufferedImage; - - import java.io.InputStream; + import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.text.MessageFormat; - import java.util.ArrayList; - import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.ResourceBundle; import jexer.backend.ECMA48Terminal; import jexer.backend.GlyphMaker; - import jexer.backend.MultiScreen; import jexer.backend.SwingTerminal; import jexer.bits.Cell; - import jexer.bits.CellAttributes; + import jexer.event.TCommandEvent; import jexer.event.TKeypressEvent; import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; @@@ -57,13 -51,14 +51,14 @@@ import jexer.menu.TMenu import jexer.tterminal.DisplayLine; import jexer.tterminal.DisplayListener; import jexer.tterminal.ECMA48; + import static jexer.TCommand.*; import static jexer.TKeypress.*; /** * TTerminalWidget exposes a ECMA-48 / ANSI X3.64 style terminal in a widget. */ public class TTerminalWidget extends TScrollableWidget - implements DisplayListener { + implements DisplayListener, EditMenuUser { /** * Translated strings. @@@ -84,6 -79,11 +79,11 @@@ */ private Process shell; + /** + * If true, something called 'ptypipe' is on the PATH and executable. + */ + private static boolean ptypipeOnPath = false; + /** * If true, we are using the ptypipe utility to support dynamic window * resizing. ptypipe is available at @@@ -163,6 -163,13 +163,13 @@@ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Static constructor. + */ + static { + checkForPtypipe(); + } + /** * Public constructor spawns a custom command line. * @@@ -198,7 -205,7 +205,7 @@@ * @param x column relative to parent * @param y row relative to parent * @param command the command line to execute - * @param closeAction action to perform when the shell sxits + * @param closeAction action to perform when the shell exits */ public TTerminalWidget(final TWidget parent, final int x, final int y, final String [] command, final TAction closeAction) { @@@ -215,7 -222,7 +222,7 @@@ * @param width width of widget * @param height height of widget * @param command the command line to execute - * @param closeAction action to perform when the shell sxits + * @param closeAction action to perform when the shell exits */ public TTerminalWidget(final TWidget parent, final int x, final int y, final int width, final int height, final String [] command, @@@ -236,6 -243,14 +243,14 @@@ fullCommand = new String[command.length + 1]; fullCommand[0] = "ptypipe"; System.arraycopy(command, 0, fullCommand, 1, command.length); + } else if (System.getProperty("jexer.TTerminal.ptypipe", + "auto").equals("auto") + && (ptypipeOnPath == true) + ) { + ptypipe = true; + fullCommand = new String[command.length + 1]; + fullCommand[0] = "ptypipe"; + System.arraycopy(command, 0, fullCommand, 1, command.length); } else if (System.getProperty("os.name").startsWith("Windows")) { fullCommand = new String[3]; fullCommand[0] = "cmd"; @@@ -251,12 -266,24 +266,24 @@@ fullCommand[5] = stringArrayToString(command); } else { // Default: behave like Linux - fullCommand = new String[5]; - fullCommand[0] = "script"; - fullCommand[1] = "-fqe"; - fullCommand[2] = "/dev/null"; - fullCommand[3] = "-c"; - fullCommand[4] = stringArrayToString(command); + if (System.getProperty("jexer.TTerminal.setsid", + "true").equals("false") + ) { + fullCommand = new String[5]; + fullCommand[0] = "script"; + fullCommand[1] = "-fqe"; + fullCommand[2] = "/dev/null"; + fullCommand[3] = "-c"; + fullCommand[4] = stringArrayToString(command); + } else { + fullCommand = new String[6]; + fullCommand[0] = "setsid"; + fullCommand[1] = "script"; + fullCommand[2] = "-fqe"; + fullCommand[3] = "/dev/null"; + fullCommand[4] = "-c"; + fullCommand[5] = stringArrayToString(command); + } } spawnShell(fullCommand); } @@@ -278,7 -305,7 +305,7 @@@ * @param parent parent widget * @param x column relative to parent * @param y row relative to parent - * @param closeAction action to perform when the shell sxits + * @param closeAction action to perform when the shell exits */ public TTerminalWidget(final TWidget parent, final int x, final int y, final TAction closeAction) { @@@ -294,7 -321,7 +321,7 @@@ * @param y row relative to parent * @param width width of widget * @param height height of widget - * @param closeAction action to perform when the shell sxits + * @param closeAction action to perform when the shell exits */ public TTerminalWidget(final TWidget parent, final int x, final int y, final int width, final int height, final TAction closeAction) { @@@ -320,6 -347,7 +347,7 @@@ // GNU differ on the '-f' vs '-F' flags, we need two different // commands. Lovely. String cmdShellGNU = "script -fqe /dev/null"; + String cmdShellGNUSetsid = "setsid script -fqe /dev/null"; String cmdShellBSD = "script -q -F /dev/null"; // ptypipe is another solution that permits dynamic window resizing. @@@ -332,12 -360,24 +360,24 @@@ ) { ptypipe = true; spawnShell(cmdShellPtypipe.split("\\s+")); + } else if (System.getProperty("jexer.TTerminal.ptypipe", + "auto").equals("auto") + && (ptypipeOnPath == true) + ) { + ptypipe = true; + spawnShell(cmdShellPtypipe.split("\\s+")); } else if (System.getProperty("os.name").startsWith("Windows")) { spawnShell(cmdShellWindows.split("\\s+")); } else if (System.getProperty("os.name").startsWith("Mac")) { spawnShell(cmdShellBSD.split("\\s+")); } else if (System.getProperty("os.name").startsWith("Linux")) { - spawnShell(cmdShellGNU.split("\\s+")); + if (System.getProperty("jexer.TTerminal.setsid", + "true").equals("false") + ) { + spawnShell(cmdShellGNU.split("\\s+")); + } else { + spawnShell(cmdShellGNUSetsid.split("\\s+")); + } } else { // When all else fails, assume GNU. spawnShell(cmdShellGNU.split("\\s+")); @@@ -525,6 -565,32 +565,32 @@@ super.onMouseMotion(mouse); } + /** + * Handle posted command events. + * + * @param command command event + */ + @Override + public void onCommand(final TCommandEvent command) { + if (emulator == null) { + return; + } + + if (command.equals(cmPaste)) { + // Paste text from clipboard. + String text = getClipboard().pasteText(); + if (text != null) { + for (int i = 0; i < text.length(); ) { + int ch = text.codePointAt(i); + emulator.addUserEvent(new TKeypressEvent(false, 0, ch, + false, false, false)); + i += Character.charCount(ch); + } + } + return; + } + } + // ------------------------------------------------------------------------ // TScrollableWidget ------------------------------------------------------ // ------------------------------------------------------------------------ @@@ -541,9 -607,7 +607,7 @@@ int width = getDisplayWidth(); boolean syncEmulator = false; - if ((System.currentTimeMillis() - lastUpdateTime >= 20) - && (dirty == true) - ) { + if (System.currentTimeMillis() - lastUpdateTime >= 50) { // Too much time has passed, draw it all. syncEmulator = true; } else if (emulator.isReading() && (dirty == false)) { @@@ -731,6 -795,43 +795,43 @@@ // TTerminalWidget -------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Check for 'ptypipe' on the path. If available, set ptypipeOnPath. + */ + private static void checkForPtypipe() { + String systemPath = System.getenv("PATH"); + if (systemPath == null) { + return; + } + + String [] paths = systemPath.split(File.pathSeparator); + if (paths == null) { + return; + } + if (paths.length == 0) { + return; + } + for (int i = 0; i < paths.length; i++) { + File path = new File(paths[i]); + if (path.exists() && path.isDirectory()) { + File [] files = path.listFiles(); + if (files == null) { + continue; + } + if (files.length == 0) { + continue; + } + for (int j = 0; j < files.length; j++) { + File file = files[j]; + if (file.canExecute() && file.getName().equals("ptypipe")) { + ptypipeOnPath = true; + return; + } + } + } + } + } + /** * Get the desired window title. * @@@ -884,10 -985,7 +985,7 @@@ } }); } - if (getApplication() != null) { - getApplication().postEvent(new TMenuEvent( - TMenu.MID_REPAINT)); - } + app.doRepaint(); } } @@@ -957,6 -1055,19 +1055,19 @@@ } // synchronized (emulator) } + /** + * Wait for a period of time to get output from the launched process. + * + * @param millis millis to wait for, or 0 to wait forever + * @return true if the launched process has emitted something + */ + public boolean waitForOutput(final int millis) { + if (emulator == null) { + return false; + } + return emulator.waitForOutput(millis); + } + /** * Check if a mouse press/release/motion event coordinate is over the * emulator. @@@ -1125,7 -1236,17 +1236,17 @@@ * Called by emulator when fresh data has come in. */ public void displayChanged() { - dirty = true; + if (emulator != null) { + // Force sync here: EMCA48.run() thread might be setting + // dirty=true while TTerminalWdiget.draw() is setting + // dirty=false. If these writes start interleaving, the display + // stops getting updated. + synchronized (emulator) { + dirty = true; + } + } else { + dirty = true; + } getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT)); } @@@ -1153,4 -1274,53 +1274,53 @@@ return 24; } + /** + * Get the exit value for the emulator. + * + * @return exit value + */ + public int getExitValue() { + return exitValue; + } + + // ------------------------------------------------------------------------ + // EditMenuUser ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Check if the cut menu item should be enabled. + * + * @return true if the cut menu item should be enabled + */ + public boolean isEditMenuCut() { + return false; + } + + /** + * Check if the copy menu item should be enabled. + * + * @return true if the copy menu item should be enabled + */ + public boolean isEditMenuCopy() { + return false; + } + + /** + * Check if the paste menu item should be enabled. + * + * @return true if the paste menu item should be enabled + */ + public boolean isEditMenuPaste() { + return true; + } + + /** + * Check if the clear menu item should be enabled. + * + * @return true if the clear menu item should be enabled + */ + public boolean isEditMenuClear() { + return false; + } + } diff --combined TTerminalWindow.java index e96c50c,754b7a5..754b7a5 --- a/TTerminalWindow.java +++ b/TTerminalWindow.java @@@ -28,35 -28,14 +28,14 @@@ */ package jexer; - import java.awt.Font; - import java.awt.FontMetrics; - import java.awt.Graphics2D; - import java.awt.image.BufferedImage; - - import java.io.InputStream; - import java.io.IOException; - import java.lang.reflect.Field; - import java.text.MessageFormat; - import java.util.ArrayList; - import java.util.HashMap; - import java.util.List; - import java.util.Map; import java.util.ResourceBundle; - import jexer.backend.ECMA48Terminal; - import jexer.backend.GlyphMaker; - import jexer.backend.MultiScreen; - import jexer.backend.SwingTerminal; - import jexer.bits.Cell; - import jexer.bits.CellAttributes; + import jexer.menu.TMenu; import jexer.event.TKeypressEvent; import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; - import jexer.menu.TMenu; - import jexer.tterminal.DisplayLine; - import jexer.tterminal.DisplayListener; - import jexer.tterminal.ECMA48; + import static jexer.TCommand.*; import static jexer.TKeypress.*; /** @@@ -163,10 -142,14 +142,14 @@@ public class TTerminalWindow extends TS addShortcutKeys(); // Add shortcut text - newStatusBar(i18n.getString("statusBarRunning")); + TStatusBar statusBar = newStatusBar(i18n.getString("statusBarRunning")); + statusBar.addShortcutKeypress(kbF1, cmHelp, + i18n.getString("statusBarHelp")); + statusBar.addShortcutKeypress(kbF10, cmMenu, + i18n.getString("statusBarMenu")); // Spin it up - terminal = new TTerminalWidget(this, 0, 0, new TAction() { + terminal = new TTerminalWidget(this, 0, 0, command, new TAction() { public void DO() { onShellExit(); } @@@ -215,7 -198,11 +198,11 @@@ addShortcutKeys(); // Add shortcut text - newStatusBar(i18n.getString("statusBarRunning")); + TStatusBar statusBar = newStatusBar(i18n.getString("statusBarRunning")); + statusBar.addShortcutKeypress(kbF1, cmHelp, + i18n.getString("statusBarHelp")); + statusBar.addShortcutKeypress(kbF10, cmMenu, + i18n.getString("statusBarMenu")); // Spin it up terminal = new TTerminalWidget(this, 0, 0, new TAction() { @@@ -283,7 -270,10 +270,10 @@@ */ @Override public void onKeypress(final TKeypressEvent keypress) { - if ((terminal != null) && (terminal.isReading())) { + if ((terminal != null) + && (terminal.isReading()) + && (!inKeyboardResize) + ) { terminal.onKeypress(keypress); } else { super.onKeypress(keypress); @@@ -352,6 -342,16 +342,16 @@@ } } + /** + * Get this window's help topic to load. + * + * @return the topic name + */ + @Override + public String getHelpTopic() { + return "Terminal Window"; + } + // ------------------------------------------------------------------------ // TTerminalWindow -------------------------------------------------------- // ------------------------------------------------------------------------ @@@ -452,4 -452,29 +452,29 @@@ getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT)); } + /** + * Wait for a period of time to get output from the launched process. + * + * @param millis millis to wait for, or 0 to wait forever + * @return true if the launched process has emitted something + */ + public boolean waitForOutput(final int millis) { + if (terminal == null) { + return false; + } + return terminal.waitForOutput(millis); + } + + /** + * Get the exit value for the emulator. + * + * @return exit value + */ + public int getExitValue() { + if (terminal == null) { + return -1; + } + return terminal.getExitValue(); + } + } diff --combined TTerminalWindow.properties index ed22f49,44a19f6..44a19f6 --- a/TTerminalWindow.properties +++ b/TTerminalWindow.properties @@@ -1,2 -1,4 +1,4 @@@ windowTitle=Terminal statusBarRunning=Terminal session executing... + statusBarHelp=Help + statusBarMenu=Menu diff --combined TText.java index 22bc4b8,f6d7feb..f6d7feb --- a/TText.java +++ b/TText.java @@@ -29,7 -29,7 +29,7 @@@ package jexer; import java.util.Arrays; - import java.util.LinkedList; + import java.util.ArrayList; import java.util.List; import jexer.bits.CellAttributes; @@@ -162,7 -162,7 +162,7 @@@ public class TText extends TScrollableW this.text = text; this.colorKey = colorKey; - lines = new LinkedList(); + lines = new ArrayList(); vScroller = new TVScroller(this, getWidth() - 1, 0, Math.max(1, getHeight() - 1)); @@@ -403,7 -403,7 +403,7 @@@ /** * Set justification. * - * @param justification LEFT, CENTER, RIGHT, or FULL + * @param justification NONE, LEFT, CENTER, RIGHT, or FULL */ public void setJustification(final Justification justification) { this.justification = justification; @@@ -442,4 -442,12 +442,12 @@@ reflowData(); } + /** + * Un-justify the text. + */ + public void unJustify() { + justification = Justification.NONE; + reflowData(); + } + } diff --combined TWidget.java index eb06175,5c93712..32ed806 --- a/TWidget.java +++ b/TWidget.java @@@ -36,6 -36,7 +36,7 @@@ import java.util.ArrayList import jexer.backend.Screen; import jexer.bits.Cell; import jexer.bits.CellAttributes; + import jexer.bits.Clipboard; import jexer.bits.ColorTheme; import jexer.event.TCommandEvent; import jexer.event.TInputEvent; @@@ -184,7 -185,14 +185,7 @@@ public abstract class TWidget implement * @param enabled if true assume enabled */ protected TWidget(final TWidget parent, final boolean enabled) { - this.enabled = enabled; - this.parent = parent; - children = new ArrayList(); - - if (parent != null) { - this.window = parent.window; - parent.addChild(this); - } + this(parent, enabled, 0, 0, 0, 0); } /** @@@ -591,9 -599,8 +592,8 @@@ * @param command command event */ public void onCommand(final TCommandEvent command) { - // Default: do nothing, pass to children instead - for (TWidget widget: children) { - widget.onCommand(command); + if (activeChild != null) { + activeChild.onCommand(command); } } @@@ -1126,6 -1133,18 +1126,18 @@@ return null; } + /** + * Get the Clipboard. + * + * @return the Clipboard, or null if not assigned + */ + public Clipboard getClipboard() { + if (window != null) { + return window.getApplication().getClipboard(); + } + return null; + } + /** * Comparison operator. For various subclasses it sorts on: *
    @@@ -1138,8 -1157,7 +1150,8 @@@ * @return difference between this.tabOrder and that.tabOrder, or * difference between this.z and that.z, or String.compareTo(text) */ + @Override - public final int compareTo(final TWidget that) { + public int compareTo(final TWidget that) { if ((this instanceof TWindow) && (that instanceof TWindow) ) { @@@ -1392,29 -1410,6 +1404,29 @@@ children.get(i).tabOrder = i; } } + + /** + * Remove and {@link TWidget#close()} the given child from this {@link TWidget}. + *

    + * Will also reorder the tab values of the remaining children. + * + * @param child the child to remove + * + * @return TRUE if the child was removed, FALSE if it was not found + */ + public boolean removeChild(final TWidget child) { + if (children.remove(child)) { + child.close(); + child.parent = null; + child.window = null; + + resetTabOrder(); + + return true; + } + + return false; + } /** * Switch the active child. @@@ -1439,9 -1434,9 +1451,9 @@@ if (activeChild != null) { activeChild.active = false; } - child.active = true; - activeChild = child; } + child.active = true; + activeChild = child; } } @@@ -2168,6 -2163,21 +2180,21 @@@ return new TRadioGroup(this, x, y, label); } + /** + * Convenience function to add a radio button group to this + * container/window. + * + * @param x column relative to parent + * @param y row relative to parent + * @param width width of group + * @param label label to display on the group box + */ + public final TRadioGroup addRadioGroup(final int x, final int y, + final int width, final String label) { + + return new TRadioGroup(this, x, y, width, label); + } + /** * Convenience function to add a text field to this container/window. * diff --combined TWindow.java index 58195c9,4d14d0e..4d14d0e --- a/TWindow.java +++ b/TWindow.java @@@ -199,6 -199,11 +199,11 @@@ public class TWindow extends TWidget */ private boolean hideMouse = false; + /** + * The help topic for this window. + */ + protected String helpTopic = "Help"; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@@ -541,12 -546,6 +546,6 @@@ } if (inWindowResize) { - // Do not permit resizing below the status line - if (mouse.getAbsoluteY() == application.getDesktopBottom()) { - inWindowResize = false; - return; - } - // Move window over setWidth(resizeWindowWidth + (mouse.getAbsoluteX() - moveWindowMouseX)); @@@ -566,23 -565,22 +565,22 @@@ // Keep within min/max bounds if (getWidth() < minimumWindowWidth) { setWidth(minimumWindowWidth); - inWindowResize = false; } if (getHeight() < minimumWindowHeight) { setHeight(minimumWindowHeight); - inWindowResize = false; } if ((maximumWindowWidth > 0) && (getWidth() > maximumWindowWidth) ) { setWidth(maximumWindowWidth); - inWindowResize = false; } if ((maximumWindowHeight > 0) && (getHeight() > maximumWindowHeight) ) { setHeight(maximumWindowHeight); - inWindowResize = false; + } + if (getHeight() + getY() >= getApplication().getDesktopBottom()) { + setHeight(getApplication().getDesktopBottom() - getY()); } // Pass a resize event to my children @@@ -1440,6 -1438,15 +1438,15 @@@ this.hideMouse = hideMouse; } + /** + * Get this window's help topic to load. + * + * @return the topic name + */ + public String getHelpTopic() { + return helpTopic; + } + /** * Generate a human-readable string for this window. * @@@ -1447,8 -1454,9 +1454,9 @@@ */ @Override public String toString() { - return String.format("%s(%8x) \'%s\' position (%d, %d) geometry %dx%d" + - " hidden %s modal %s", getClass().getName(), hashCode(), title, + return String.format("%s(%8x) \'%s\' Z %d position (%d, %d) " + + "geometry %dx%d hidden %s modal %s", + getClass().getName(), hashCode(), title, getZ(), getX(), getY(), getWidth(), getHeight(), hidden, isModal()); } diff --combined backend/ECMA48Terminal.java index e2997d2,429e698..429e698 --- a/backend/ECMA48Terminal.java +++ b/backend/ECMA48Terminal.java @@@ -28,6 -28,9 +28,9 @@@ */ package jexer.backend; + import java.awt.Graphics; + import java.awt.Graphics2D; + import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; @@@ -47,10 -50,10 +50,10 @@@ import java.util.HashMap import java.util.List; import javax.imageio.ImageIO; - import jexer.TImage; import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.bits.Color; + import jexer.bits.StringUtils; import jexer.event.TCommandEvent; import jexer.event.TInputEvent; import jexer.event.TKeypressEvent; @@@ -83,6 -86,16 +86,16 @@@ public class ECMA48Terminal extends Log MOUSE_SGR, } + /** + * Available Jexer images support. + */ + private enum JexerImageOption { + DISABLED, + JPG, + PNG, + RGB, + } + // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ @@@ -189,6 -202,11 +202,11 @@@ */ private boolean sixel = true; + /** + * If true, use a single shared palette for sixel. + */ + private boolean sixelSharedPalette = true; + /** * The sixel palette handler. */ @@@ -217,20 -235,16 +235,16 @@@ private ImageCache iterm2Cache = null; /** - * If true, emit image data via Jexer image protocol. + * If not DISABLED, emit image data via Jexer image protocol if the + * terminal supports it. */ - private boolean jexerImages = false; + private JexerImageOption jexerImageOption = JexerImageOption.JPG; /** * The Jexer post-rendered string cache. */ private ImageCache jexerCache = null; - /** - * Base64 encoder used by iTerm2 and Jexer images. - */ - private java.util.Base64.Encoder base64 = null; - /** * If true, then we changed System.in and need to change it back. */ @@@ -1156,11 -1170,12 +1170,12 @@@ // Enable mouse reporting and metaSendsEscape this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true)); - this.output.flush(); // Request xterm use the sixel settings we want this.output.printf("%s", xtermSetSixelSettings()); + this.output.flush(); + // Query the screen size sessionInfo.queryWindowSize(); setDimensions(sessionInfo.getWindowWidth(), @@@ -1248,11 -1263,12 +1263,12 @@@ // Enable mouse reporting and metaSendsEscape this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true)); - this.output.flush(); // Request xterm use the sixel settings we want this.output.printf("%s", xtermSetSixelSettings()); + this.output.flush(); + // Query the screen size sessionInfo.queryWindowSize(); setDimensions(sessionInfo.getWindowWidth(), @@@ -1479,7 -1495,15 +1495,15 @@@ // SQUASH } - // Default to using images for full-width characters. + // Shared palette + if (System.getProperty("jexer.ECMA48.sixelSharedPalette", + "true").equals("false")) { + sixelSharedPalette = false; + } else { + sixelSharedPalette = true; + } + + // Default to not supporting iTerm2 images. if (System.getProperty("jexer.ECMA48.iTerm2Images", "false").equals("true")) { iterm2Images = true; @@@ -1487,6 -1511,19 +1511,19 @@@ iterm2Images = false; } + // Default to using JPG Jexer images if terminal supports it. + String jexerImageStr = System.getProperty("jexer.ECMA48.jexerImages", + "jpg").toLowerCase(); + if (jexerImageStr.equals("false")) { + jexerImageOption = JexerImageOption.DISABLED; + } else if (jexerImageStr.equals("jpg")) { + jexerImageOption = JexerImageOption.JPG; + } else if (jexerImageStr.equals("png")) { + jexerImageOption = JexerImageOption.PNG; + } else if (jexerImageStr.equals("rgb")) { + jexerImageOption = JexerImageOption.RGB; + } + // Set custom colors setCustomSystemColors(); } @@@ -1608,7 -1645,10 +1645,10 @@@ * @return the width in pixels of a character cell */ public int getTextWidth() { - return (widthPixels / sessionInfo.getWindowWidth()); + if (sessionInfo.getWindowWidth() > 0) { + return (widthPixels / sessionInfo.getWindowWidth()); + } + return 16; } /** @@@ -1617,7 -1657,10 +1657,10 @@@ * @return the height in pixels of a character cell */ public int getTextHeight() { - return (heightPixels / sessionInfo.getWindowHeight()); + if (sessionInfo.getWindowHeight() > 0) { + return (heightPixels / sessionInfo.getWindowHeight()); + } + return 20; } /** @@@ -2052,7 -2095,7 +2095,7 @@@ if (cellsToDraw.size() > 0) { if (iterm2Images) { sb.append(toIterm2Image(x, y, cellsToDraw)); - } else if (jexerImages) { + } else if (jexerImageOption != JexerImageOption.DISABLED) { sb.append(toJexerImage(x, y, cellsToDraw)); } else { sb.append(toSixel(x, y, cellsToDraw)); @@@ -2203,10 -2246,13 +2246,13 @@@ boolean eventMouse3 = false; boolean eventMouseWheelUp = false; boolean eventMouseWheelDown = false; + boolean eventAlt = false; + boolean eventCtrl = false; + boolean eventShift = false; // System.err.printf("buttons: %04x\r\n", buttons); - switch (buttons) { + switch (buttons & 0xE3) { case 0: eventMouse1 = true; mouse1 = true; @@@ -2288,9 -2334,21 +2334,21 @@@ eventType = TMouseEvent.Type.MOUSE_MOTION; break; } + + if ((buttons & 0x04) != 0) { + eventShift = true; + } + if ((buttons & 0x08) != 0) { + eventAlt = true; + } + if ((buttons & 0x10) != 0) { + eventCtrl = true; + } + return new TMouseEvent(eventType, x, y, x, y, eventMouse1, eventMouse2, eventMouse3, - eventMouseWheelUp, eventMouseWheelDown); + eventMouseWheelUp, eventMouseWheelDown, + eventAlt, eventCtrl, eventShift); } /** @@@ -2325,12 -2383,15 +2383,15 @@@ boolean eventMouse3 = false; boolean eventMouseWheelUp = false; boolean eventMouseWheelDown = false; + boolean eventAlt = false; + boolean eventCtrl = false; + boolean eventShift = false; if (release) { eventType = TMouseEvent.Type.MOUSE_UP; } - switch (buttons) { + switch (buttons & 0xE3) { case 0: eventMouse1 = true; break; @@@ -2387,9 -2448,21 +2448,21 @@@ // Unknown, bail out return null; } + + if ((buttons & 0x04) != 0) { + eventShift = true; + } + if ((buttons & 0x08) != 0) { + eventAlt = true; + } + if ((buttons & 0x10) != 0) { + eventCtrl = true; + } + return new TMouseEvent(eventType, x, y, x, y, eventMouse1, eventMouse2, eventMouse3, - eventMouseWheelUp, eventMouseWheelDown); + eventMouseWheelUp, eventMouseWheelDown, + eventAlt, eventCtrl, eventShift); } /** @@@ -2806,6 -2879,8 +2879,8 @@@ if (decPrivateModeFlag == false) { break; } + boolean reportsJexerImages = false; + boolean reportsIterm2Images = false; for (String x: params) { if (x.equals("4")) { // Terminal reports sixel support @@@ -2818,9 -2893,27 +2893,27 @@@ if (debugToStderr) { System.err.println("Device Attributes: Jexer images"); } - jexerImages = true; + reportsJexerImages = true; + } + if (x.equals("1337")) { + // Terminal reports iTerm2 images support + if (debugToStderr) { + System.err.println("Device Attributes: iTerm2 images"); + } + reportsIterm2Images = true; } } + if (reportsJexerImages == false) { + // Terminal does not support Jexer images, disable + // them. + jexerImageOption = JexerImageOption.DISABLED; + } + if (reportsIterm2Images == false) { + // Terminal does not support iTerm2 images, disable + // them. + iterm2Images = false; + } + resetParser(); return; case 't': // windowOps @@@ -2900,12 -2993,16 +2993,16 @@@ * - enable sixel scrolling * * - disable private color registers (so that we can use one common - * palette) + * palette) if sixelSharedPalette is set * * @return the string to emit to xterm */ private String xtermSetSixelSettings() { - return "\033[?80h\033[?1070l"; + if (sixelSharedPalette == true) { + return "\033[?80h\033[?1070l"; + } else { + return "\033[?80h\033[?1070h"; + } } /** @@@ -3022,8 -3119,9 +3119,9 @@@ if (palette == null) { palette = new SixelPalette(); - // TODO: make this an option (shared palette or not) - palette.emitPalette(sb, null); + if (sixelSharedPalette == true) { + palette.emitPalette(sb, null); + } } return sb.toString(); @@@ -3071,9 -3169,8 +3169,8 @@@ if (y == height - 1) { // We are on the bottom row. If scrolling mode is enabled // (default), then VT320/xterm will scroll the entire screen if - // we draw any pixels here. - - // TODO: support sixel scrolling mode disabled as an option. + // we draw any pixels here. Do not draw the image, bail out + // instead. sb.append(normal()); sb.append(gotoXY(x, y)); for (int j = 0; j < cells.size(); j++) { @@@ -3106,112 -3203,15 +3203,15 @@@ // System.err.println("CACHE MISS"); } - int imageWidth = cells.get(0).getImage().getWidth(); - int imageHeight = cells.get(0).getImage().getHeight(); - - // cells.get(x).getImage() has a dithered bitmap containing indexes - // into the color palette. Piece these together into one larger - // image for final rendering. - int totalWidth = 0; - int fullWidth = cells.size() * getTextWidth(); - int fullHeight = getTextHeight(); - for (int i = 0; i < cells.size(); i++) { - totalWidth += cells.get(i).getImage().getWidth(); - } - - BufferedImage image = new BufferedImage(fullWidth, - fullHeight, BufferedImage.TYPE_INT_ARGB); - - int [] rgbArray; - for (int i = 0; i < cells.size() - 1; i++) { - int tileWidth = Math.min(cells.get(i).getImage().getWidth(), - imageWidth); - int tileHeight = Math.min(cells.get(i).getImage().getHeight(), - imageHeight); - - if (false && cells.get(i).isInvertedImage()) { - // I used to put an all-white cell over the cursor, don't do - // that anymore. - rgbArray = new int[imageWidth * imageHeight]; - for (int j = 0; j < rgbArray.length; j++) { - rgbArray[j] = 0xFFFFFF; - } - } else { - try { - rgbArray = cells.get(i).getImage().getRGB(0, 0, - tileWidth, tileHeight, null, 0, tileWidth); - } catch (Exception e) { - throw new RuntimeException("image " + imageWidth + "x" + - imageHeight + - "tile " + tileWidth + "x" + - tileHeight + - " cells.get(i).getImage() " + - cells.get(i).getImage() + - " i " + i + - " fullWidth " + fullWidth + - " fullHeight " + fullHeight, e); - } - } - - /* - System.err.printf("calling image.setRGB(): %d %d %d %d %d\n", - i * imageWidth, 0, imageWidth, imageHeight, - 0, imageWidth); - System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n", - fullWidth, fullHeight, cells.size(), getTextWidth()); - */ - - image.setRGB(i * imageWidth, 0, tileWidth, tileHeight, - rgbArray, 0, tileWidth); - if (tileHeight < fullHeight) { - int backgroundColor = cells.get(i).getBackground().getRGB(); - for (int imageX = 0; imageX < image.getWidth(); imageX++) { - for (int imageY = imageHeight; imageY < fullHeight; - imageY++) { - - image.setRGB(imageX, imageY, backgroundColor); - } - } - } - } - totalWidth -= ((cells.size() - 1) * imageWidth); - if (false && cells.get(cells.size() - 1).isInvertedImage()) { - // I used to put an all-white cell over the cursor, don't do that - // anymore. - rgbArray = new int[totalWidth * imageHeight]; - for (int j = 0; j < rgbArray.length; j++) { - rgbArray[j] = 0xFFFFFF; - } - } else { - try { - rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0, - totalWidth, imageHeight, null, 0, totalWidth); - } catch (Exception e) { - throw new RuntimeException("image " + imageWidth + "x" + - imageHeight + " cells.get(cells.size() - 1).getImage() " + - cells.get(cells.size() - 1).getImage(), e); - } - } - image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth, - imageHeight, rgbArray, 0, totalWidth); - - if (totalWidth < getTextWidth()) { - int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB(); - - for (int imageX = image.getWidth() - totalWidth; - imageX < image.getWidth(); imageX++) { - - for (int imageY = 0; imageY < fullHeight; imageY++) { - image.setRGB(imageX, imageY, backgroundColor); - } - } - } + BufferedImage image = cellsToImage(cells); + int fullHeight = image.getHeight(); // Dither the image. It is ok to lose the original here. if (palette == null) { palette = new SixelPalette(); - // TODO: make this an option (shared palette or not) - palette.emitPalette(sb, null); + if (sixelSharedPalette == true) { + palette.emitPalette(sb, null); + } } image = palette.ditherImage(image); @@@ -3219,20 -3219,17 +3219,17 @@@ int rasterHeight = 0; int rasterWidth = image.getWidth(); - /* - - // TODO: make this an option (shared palette or not) - - // Emit the palette, but only for the colors actually used by these - // cells. - boolean [] usedColors = new boolean[sixelPaletteSize]; - for (int imageX = 0; imageX < image.getWidth(); imageX++) { - for (int imageY = 0; imageY < image.getHeight(); imageY++) { - usedColors[image.getRGB(imageX, imageY)] = true; + if (sixelSharedPalette == false) { + // Emit the palette, but only for the colors actually used by + // these cells. + boolean [] usedColors = new boolean[sixelPaletteSize]; + for (int imageX = 0; imageX < image.getWidth(); imageX++) { + for (int imageY = 0; imageY < image.getHeight(); imageY++) { + usedColors[image.getRGB(imageX, imageY)] = true; + } } + palette.emitPalette(sb, usedColors); } - palette.emitPalette(sb, usedColors); - */ // Render the entire row of cells. for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) { @@@ -3362,74 -3359,23 +3359,23 @@@ return sixel; } - // ------------------------------------------------------------------------ - // End sixel output support ----------------------------------------------- - // ------------------------------------------------------------------------ - - // ------------------------------------------------------------------------ - // iTerm2 image output support -------------------------------------------- - // ------------------------------------------------------------------------ - /** - * Create an iTerm2 images string representing a row of several cells - * containing bitmap data. + * Convert a horizontal range of cell's image data into a single + * contigous image, rescaled and anti-aliased to match the current text + * cell size. * - * @param x column coordinate. 0 is the left-most column. - * @param y row coordinate. 0 is the top-most row. - * @param cells the cells containing the bitmap data - * @return the string to emit to an ANSI / ECMA-style terminal + * @param cells the cells containing image data + * @return the image resized to the current text cell size */ - private String toIterm2Image(final int x, final int y, - final ArrayList cells) { - - StringBuilder sb = new StringBuilder(); - - assert (cells != null); - assert (cells.size() > 0); - assert (cells.get(0).getImage() != null); - - if (iterm2Images == false) { - sb.append(normal()); - sb.append(gotoXY(x, y)); - for (int i = 0; i < cells.size(); i++) { - sb.append(' '); - } - return sb.toString(); - } - - if (iterm2Cache == null) { - iterm2Cache = new ImageCache(height * 10); - base64 = java.util.Base64.getEncoder(); - } - - // Save and get rows to/from the cache that do NOT have inverted - // cells. - boolean saveInCache = true; - for (Cell cell: cells) { - if (cell.isInvertedImage()) { - saveInCache = false; - } - } - if (saveInCache) { - String cachedResult = iterm2Cache.get(cells); - if (cachedResult != null) { - // System.err.println("CACHE HIT"); - sb.append(gotoXY(x, y)); - sb.append(cachedResult); - return sb.toString(); - } - // System.err.println("CACHE MISS"); - } - + private BufferedImage cellsToImage(final List cells) { int imageWidth = cells.get(0).getImage().getWidth(); int imageHeight = cells.get(0).getImage().getHeight(); - // cells.get(x).getImage() has a dithered bitmap containing indexes - // into the color palette. Piece these together into one larger + // Piece cells.get(x).getImage() pieces together into one larger // image for final rendering. int totalWidth = 0; - int fullWidth = cells.size() * getTextWidth(); - int fullHeight = getTextHeight(); + int fullWidth = cells.size() * imageWidth; + int fullHeight = imageHeight; for (int i = 0; i < cells.size(); i++) { totalWidth += cells.get(i).getImage().getWidth(); } @@@ -3439,10 -3385,9 +3385,9 @@@ int [] rgbArray; for (int i = 0; i < cells.size() - 1; i++) { - int tileWidth = Math.min(cells.get(i).getImage().getWidth(), - imageWidth); - int tileHeight = Math.min(cells.get(i).getImage().getHeight(), - imageHeight); + int tileWidth = imageWidth; + int tileHeight = imageHeight; + if (false && cells.get(i).isInvertedImage()) { // I used to put an all-white cell over the cursor, don't do // that anymore. @@@ -3509,7 -3454,7 +3454,7 @@@ image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth, imageHeight, rgbArray, 0, totalWidth); - if (totalWidth < getTextWidth()) { + if (totalWidth < imageWidth) { int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB(); for (int imageX = image.getWidth() - totalWidth; @@@ -3521,6 -3466,91 +3466,91 @@@ } } + if ((image.getWidth() != cells.size() * getTextWidth()) + || (image.getHeight() != getTextHeight()) + ) { + // Rescale the image to fit the text cells it is going into. + BufferedImage newImage; + newImage = new BufferedImage(cells.size() * getTextWidth(), + getTextHeight(), BufferedImage.TYPE_INT_ARGB); + + Graphics gr = newImage.getGraphics(); + if (gr instanceof Graphics2D) { + ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + } + gr.drawImage(image, 0, 0, newImage.getWidth(), + newImage.getHeight(), null, null); + gr.dispose(); + image = newImage; + } + + return image; + } + + // ------------------------------------------------------------------------ + // End sixel output support ----------------------------------------------- + // ------------------------------------------------------------------------ + + // ------------------------------------------------------------------------ + // iTerm2 image output support -------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Create an iTerm2 images string representing a row of several cells + * containing bitmap data. + * + * @param x column coordinate. 0 is the left-most column. + * @param y row coordinate. 0 is the top-most row. + * @param cells the cells containing the bitmap data + * @return the string to emit to an ANSI / ECMA-style terminal + */ + private String toIterm2Image(final int x, final int y, + final ArrayList cells) { + + StringBuilder sb = new StringBuilder(); + + assert (cells != null); + assert (cells.size() > 0); + assert (cells.get(0).getImage() != null); + + if (iterm2Images == false) { + sb.append(normal()); + sb.append(gotoXY(x, y)); + for (int i = 0; i < cells.size(); i++) { + sb.append(' '); + } + return sb.toString(); + } + + if (iterm2Cache == null) { + iterm2Cache = new ImageCache(height * 10); + } + + // Save and get rows to/from the cache that do NOT have inverted + // cells. + boolean saveInCache = true; + for (Cell cell: cells) { + if (cell.isInvertedImage()) { + saveInCache = false; + } + } + if (saveInCache) { + String cachedResult = iterm2Cache.get(cells); + if (cachedResult != null) { + // System.err.println("CACHE HIT"); + sb.append(gotoXY(x, y)); + sb.append(cachedResult); + return sb.toString(); + } + // System.err.println("CACHE MISS"); + } + + BufferedImage image = cellsToImage(cells); + int fullHeight = image.getHeight(); + /* * From https://iterm2.com/documentation-images.html: * @@@ -3582,8 -3612,6 +3612,6 @@@ return ""; } - // iTerm2 does not advance the cursor automatically, so place it - // myself. sb.append("\033]1337;File="); /* sb.append(String.format("width=$d;height=1;preserveAspectRatio=1;", @@@ -3595,7 -3623,7 +3623,7 @@@ getTextHeight()))); */ sb.append("inline=1:"); - sb.append(base64.encodeToString(pngOutputStream.toByteArray())); + sb.append(StringUtils.toBase64(pngOutputStream.toByteArray())); sb.append("\007"); if (saveInCache) { @@@ -3641,7 -3669,7 +3669,7 @@@ assert (cells.size() > 0); assert (cells.get(0).getImage() != null); - if (jexerImages == false) { + if (jexerImageOption == JexerImageOption.DISABLED) { sb.append(normal()); sb.append(gotoXY(x, y)); for (int i = 0; i < cells.size(); i++) { @@@ -3652,7 -3680,6 +3680,6 @@@ if (jexerCache == null) { jexerCache = new ImageCache(height * 10); - base64 = java.util.Base64.getEncoder(); } // Save and get rows to/from the cache that do NOT have inverted @@@ -3674,121 -3701,80 +3701,80 @@@ // System.err.println("CACHE MISS"); } - int imageWidth = cells.get(0).getImage().getWidth(); - int imageHeight = cells.get(0).getImage().getHeight(); - - // cells.get(x).getImage() has a dithered bitmap containing indexes - // into the color palette. Piece these together into one larger - // image for final rendering. - int totalWidth = 0; - int fullWidth = cells.size() * getTextWidth(); - int fullHeight = getTextHeight(); - for (int i = 0; i < cells.size(); i++) { - totalWidth += cells.get(i).getImage().getWidth(); - } - - BufferedImage image = new BufferedImage(fullWidth, - fullHeight, BufferedImage.TYPE_INT_ARGB); + BufferedImage image = cellsToImage(cells); + int fullHeight = image.getHeight(); - int [] rgbArray; - for (int i = 0; i < cells.size() - 1; i++) { - int tileWidth = Math.min(cells.get(i).getImage().getWidth(), - imageWidth); - int tileHeight = Math.min(cells.get(i).getImage().getHeight(), - imageHeight); - if (false && cells.get(i).isInvertedImage()) { - // I used to put an all-white cell over the cursor, don't do - // that anymore. - rgbArray = new int[imageWidth * imageHeight]; - for (int j = 0; j < rgbArray.length; j++) { - rgbArray[j] = 0xFFFFFF; - } - } else { - try { - rgbArray = cells.get(i).getImage().getRGB(0, 0, - tileWidth, tileHeight, null, 0, tileWidth); - } catch (Exception e) { - throw new RuntimeException("image " + imageWidth + "x" + - imageHeight + - "tile " + tileWidth + "x" + - tileHeight + - " cells.get(i).getImage() " + - cells.get(i).getImage() + - " i " + i + - " fullWidth " + fullWidth + - " fullHeight " + fullHeight, e); + if (jexerImageOption == JexerImageOption.PNG) { + // Encode as PNG + ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream(1024); + try { + if (!ImageIO.write(image.getSubimage(0, 0, image.getWidth(), + Math.min(image.getHeight(), fullHeight)), + "PNG", pngOutputStream) + ) { + // We failed to render image, bail out. + return ""; } + } catch (IOException e) { + // We failed to render image, bail out. + return ""; } - /* - System.err.printf("calling image.setRGB(): %d %d %d %d %d\n", - i * imageWidth, 0, imageWidth, imageHeight, - 0, imageWidth); - System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n", - fullWidth, fullHeight, cells.size(), getTextWidth()); - */ - - image.setRGB(i * imageWidth, 0, tileWidth, tileHeight, - rgbArray, 0, tileWidth); - if (tileHeight < fullHeight) { - int backgroundColor = cells.get(i).getBackground().getRGB(); - for (int imageX = 0; imageX < image.getWidth(); imageX++) { - for (int imageY = imageHeight; imageY < fullHeight; - imageY++) { + sb.append("\033]444;1;0;"); + sb.append(StringUtils.toBase64(pngOutputStream.toByteArray())); + sb.append("\007"); - image.setRGB(imageX, imageY, backgroundColor); - } - } - } - } - totalWidth -= ((cells.size() - 1) * imageWidth); - if (false && cells.get(cells.size() - 1).isInvertedImage()) { - // I used to put an all-white cell over the cursor, don't do that - // anymore. - rgbArray = new int[totalWidth * imageHeight]; - for (int j = 0; j < rgbArray.length; j++) { - rgbArray[j] = 0xFFFFFF; - } - } else { - try { - rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0, - totalWidth, imageHeight, null, 0, totalWidth); - } catch (Exception e) { - throw new RuntimeException("image " + imageWidth + "x" + - imageHeight + " cells.get(cells.size() - 1).getImage() " + - cells.get(cells.size() - 1).getImage(), e); - } - } - image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth, - imageHeight, rgbArray, 0, totalWidth); + } else if (jexerImageOption == JexerImageOption.JPG) { - if (totalWidth < getTextWidth()) { - int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB(); + // Encode as JPG + ByteArrayOutputStream jpgOutputStream = new ByteArrayOutputStream(1024); - for (int imageX = image.getWidth() - totalWidth; - imageX < image.getWidth(); imageX++) { + // Convert from ARGB to RGB, otherwise the JPG encode will fail. + BufferedImage jpgImage = new BufferedImage(image.getWidth(), + image.getHeight(), BufferedImage.TYPE_INT_RGB); + int [] pixels = new int[image.getWidth() * image.getHeight()]; + image.getRGB(0, 0, image.getWidth(), image.getHeight(), pixels, + 0, image.getWidth()); + jpgImage.setRGB(0, 0, image.getWidth(), image.getHeight(), pixels, + 0, image.getWidth()); - for (int imageY = 0; imageY < fullHeight; imageY++) { - image.setRGB(imageX, imageY, backgroundColor); + try { + if (!ImageIO.write(jpgImage.getSubimage(0, 0, + jpgImage.getWidth(), + Math.min(jpgImage.getHeight(), fullHeight)), + "JPG", jpgOutputStream) + ) { + // We failed to render image, bail out. + return ""; } + } catch (IOException e) { + // We failed to render image, bail out. + return ""; } - } - sb.append(String.format("\033]444;%d;%d;0;", image.getWidth(), - Math.min(image.getHeight(), fullHeight))); + sb.append("\033]444;2;0;"); + sb.append(StringUtils.toBase64(jpgOutputStream.toByteArray())); + sb.append("\007"); + + } else if (jexerImageOption == JexerImageOption.RGB) { + + // RGB + sb.append(String.format("\033]444;0;%d;%d;0;", image.getWidth(), + Math.min(image.getHeight(), fullHeight))); - byte [] bytes = new byte[image.getWidth() * image.getHeight() * 3]; - int stride = image.getWidth(); - for (int px = 0; px < stride; px++) { - for (int py = 0; py < image.getHeight(); py++) { - int rgb = image.getRGB(px, py); - bytes[(py * stride * 3) + (px * 3)] = (byte) ((rgb >>> 16) & 0xFF); - bytes[(py * stride * 3) + (px * 3) + 1] = (byte) ((rgb >>> 8) & 0xFF); - bytes[(py * stride * 3) + (px * 3) + 2] = (byte) ( rgb & 0xFF); + byte [] bytes = new byte[image.getWidth() * image.getHeight() * 3]; + int stride = image.getWidth(); + for (int px = 0; px < stride; px++) { + for (int py = 0; py < image.getHeight(); py++) { + int rgb = image.getRGB(px, py); + bytes[(py * stride * 3) + (px * 3)] = (byte) ((rgb >>> 16) & 0xFF); + bytes[(py * stride * 3) + (px * 3) + 1] = (byte) ((rgb >>> 8) & 0xFF); + bytes[(py * stride * 3) + (px * 3) + 2] = (byte) ( rgb & 0xFF); + } } + sb.append(StringUtils.toBase64(bytes)); + sb.append("\007"); } - sb.append(base64.encodeToString(bytes)); - sb.append("\007"); if (saveInCache) { // This row is OK to save into the cache. @@@ -3804,7 -3790,7 +3790,7 @@@ * @return true if this terminal is emitting Jexer images */ public boolean hasJexerImages() { - return jexerImages; + return (jexerImageOption != JexerImageOption.DISABLED); } // ------------------------------------------------------------------------ diff --combined backend/GlyphMaker.java index 0da2918,e5fcc52..e5fcc52 --- a/backend/GlyphMaker.java +++ b/backend/GlyphMaker.java @@@ -29,6 -29,7 +29,7 @@@ package jexer.backend; import java.awt.Font; + import java.awt.FontFormatException; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.geom.Rectangle2D; @@@ -139,7 -140,7 +140,7 @@@ class GlyphMakerFont if (filename.length() == 0) { // Fallback font - font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize); + font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2); return; } @@@ -148,16 -149,16 +149,16 @@@ ClassLoader loader = Thread.currentThread().getContextClassLoader(); InputStream in = loader.getResourceAsStream(filename); fontRoot = Font.createFont(Font.TRUETYPE_FONT, in); - font = fontRoot.deriveFont(Font.PLAIN, fontSize); - } catch (java.awt.FontFormatException e) { + font = fontRoot.deriveFont(Font.PLAIN, fontSize - 2); + } catch (FontFormatException e) { // Ideally we would report an error here, either via System.err // or TExceptionDialog. However, I do not want GlyphMaker to // know about available backends, so we quietly fallback to // whatever is available as MONO. - font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize); - } catch (java.io.IOException e) { + font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2); + } catch (IOException e) { // See comment above. - font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize); + font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2); } } diff --combined backend/LogicalScreen.java index 4e4aecc,22b7e95..22b7e95 --- a/backend/LogicalScreen.java +++ b/backend/LogicalScreen.java @@@ -33,6 -33,7 +33,7 @@@ import java.awt.image.BufferedImage import jexer.backend.GlyphMaker; import jexer.bits.Cell; import jexer.bits.CellAttributes; + import jexer.bits.Clipboard; import jexer.bits.GraphicsChars; import jexer.bits.StringUtils; @@@ -1042,4 -1043,184 +1043,184 @@@ public class LogicalScreen implements S putFullwidthCharXY(x, y, cell); } + /** + * Invert the cell color at a position, including both halves of a + * double-width cell. + * + * @param x column position + * @param y row position + */ + public void invertCell(final int x, final int y) { + invertCell(x, y, false); + } + + /** + * Invert the cell color at a position. + * + * @param x column position + * @param y row position + * @param onlyThisCell if true, only invert this cell, otherwise invert + * both halves of a double-width cell if necessary + */ + public void invertCell(final int x, final int y, + final boolean onlyThisCell) { + + Cell cell = getCharXY(x, y); + if (cell.isImage()) { + cell.invertImage(); + } + 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); + } + 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 < width - 1) { + Cell rightHalf = getCharXY(x + 1, y); + if (rightHalf.getWidth() == Cell.Width.RIGHT) { + invertCell(x + 1, y, true); + return; + } + } + } + if (cell.getWidth() == Cell.Width.RIGHT) { + if (x > 0) { + Cell leftHalf = getCharXY(x - 1, y); + if (leftHalf.getWidth() == Cell.Width.LEFT) { + invertCell(x - 1, y, true); + } + } + } + } + + /** + * Set a selection area on the screen. + * + * @param x0 the starting X position of the selection + * @param y0 the starting Y position of the selection + * @param x1 the ending X position of the selection + * @param y1 the ending Y position of the selection + * @param rectangle if true, this is a rectangle select + */ + public void setSelection(final int x0, final int y0, + final int x1, final int y1, final boolean rectangle) { + + int startX = x0; + int startY = y0; + int endX = x1; + int endY = y1; + + if (((x1 < x0) && (y1 == y0)) + || (y1 < y0) + ) { + // The user dragged from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startX = x1; + startY = y1; + endX = x0; + endY = y0; + } + if (rectangle) { + for (int y = startY; y <= endY; y++) { + for (int x = startX; x <= endX; x++) { + invertCell(x, y); + } + } + } else { + if (endY > startY) { + for (int x = startX; x < width; x++) { + invertCell(x, startY); + } + for (int y = startY + 1; y < endY; y++) { + for (int x = 0; x < width; x++) { + invertCell(x, y); + } + } + for (int x = 0; x <= endX; x++) { + invertCell(x, endY); + } + } else { + assert (startY == endY); + for (int x = startX; x <= endX; x++) { + invertCell(x, startY); + } + } + } + } + + /** + * Copy the screen selection area to the clipboard. + * + * @param clipboard the clipboard to use + * @param x0 the starting X position of the selection + * @param y0 the starting Y position of the selection + * @param x1 the ending X position of the selection + * @param y1 the ending Y position of the selection + * @param rectangle if true, this is a rectangle select + */ + public void copySelection(final Clipboard clipboard, + final int x0, final int y0, final int x1, final int y1, + final boolean rectangle) { + + StringBuilder sb = new StringBuilder(); + + int startX = x0; + int startY = y0; + int endX = x1; + int endY = y1; + + if (((x1 < x0) && (y1 == y0)) + || (y1 < y0) + ) { + // The user dragged from bottom-to-top and/or right-to-left. + // Reverse the coordinates for the inverted section. + startX = x1; + startY = y1; + endX = x0; + endY = y0; + } + if (rectangle) { + for (int y = startY; y <= endY; y++) { + for (int x = startX; x <= endX; x++) { + sb.append(Character.toChars(getCharXY(x, y).getChar())); + } + sb.append("\n"); + } + } else { + if (endY > startY) { + for (int x = startX; x < width; x++) { + sb.append(Character.toChars(getCharXY(x, startY).getChar())); + } + sb.append("\n"); + for (int y = startY + 1; y < endY; y++) { + for (int x = 0; x < width; x++) { + sb.append(Character.toChars(getCharXY(x, y).getChar())); + } + sb.append("\n"); + } + for (int x = 0; x <= endX; x++) { + sb.append(Character.toChars(getCharXY(x, endY).getChar())); + } + } else { + assert (startY == endY); + for (int x = startX; x <= endX; x++) { + sb.append(Character.toChars(getCharXY(x, startY).getChar())); + } + } + } + clipboard.copyText(sb.toString()); + } + } diff --combined backend/MultiScreen.java index 9d66b69,45741c0..45741c0 --- a/backend/MultiScreen.java +++ b/backend/MultiScreen.java @@@ -33,6 -33,7 +33,7 @@@ import java.util.List import jexer.bits.Cell; import jexer.bits.CellAttributes; + import jexer.bits.Clipboard; /** * MultiScreen mirrors its I/O to several screens. @@@ -93,7 -94,10 +94,10 @@@ public class MultiScreen implements Scr * @return drawing boundary */ public int getClipRight() { - return screens.get(0).getClipRight(); + if (screens.size() > 0) { + return screens.get(0).getClipRight(); + } + return 0; } /** @@@ -113,7 -117,10 +117,10 @@@ * @return drawing boundary */ public int getClipBottom() { - return screens.get(0).getClipBottom(); + if (screens.size() > 0) { + return screens.get(0).getClipBottom(); + } + return 0; } /** @@@ -133,7 -140,10 +140,10 @@@ * @return drawing boundary */ public int getClipLeft() { - return screens.get(0).getClipLeft(); + if (screens.size() > 0) { + return screens.get(0).getClipLeft(); + } + return 0; } /** @@@ -153,7 -163,10 +163,10 @@@ * @return drawing boundary */ public int getClipTop() { - return screens.get(0).getClipTop(); + if (screens.size() > 0) { + return screens.get(0).getClipTop(); + } + return 0; } /** @@@ -190,7 -203,10 +203,10 @@@ * @return attributes at (x, y) */ public CellAttributes getAttrXY(final int x, final int y) { - return screens.get(0).getAttrXY(x, y); + if (screens.size() > 0) { + return screens.get(0).getAttrXY(x, y); + } + return new CellAttributes(); } /** @@@ -201,7 -217,10 +217,10 @@@ * @return the character + attributes */ public Cell getCharXY(final int x, final int y) { - return screens.get(0).getCharXY(x, y); + if (screens.size() > 0) { + return screens.get(0).getCharXY(x, y); + } + return new Cell(); } /** @@@ -410,7 -429,10 +429,10 @@@ */ public int getHeight() { // Return the smallest height of the screens. - int height = screens.get(0).getHeight(); + int height = 25; + if (screens.size() > 0) { + height = screens.get(0).getHeight(); + } for (Screen screen: screens) { if (screen.getHeight() < height) { height = screen.getHeight(); @@@ -426,7 -448,10 +448,10 @@@ */ public int getWidth() { // Return the smallest width of the screens. - int width = screens.get(0).getWidth(); + int width = 80; + if (screens.size() > 0) { + width = screens.get(0).getWidth(); + } for (Screen screen: screens) { if (screen.getWidth() < width) { width = screen.getWidth(); @@@ -582,7 -607,10 +607,10 @@@ * @return true if the cursor is visible */ public boolean isCursorVisible() { - return screens.get(0).isCursorVisible(); + if (screens.size() > 0) { + return screens.get(0).isCursorVisible(); + } + return true; } /** @@@ -591,7 -619,10 +619,10 @@@ * @return the cursor x column position */ public int getCursorX() { - return screens.get(0).getCursorX(); + if (screens.size() > 0) { + return screens.get(0).getCursorX(); + } + return 0; } /** @@@ -600,7 -631,10 +631,10 @@@ * @return the cursor y row position */ public int getCursorY() { - return screens.get(0).getCursorY(); + if (screens.size() > 0) { + return screens.get(0).getCursorY(); + } + return 0; } /** @@@ -670,4 -704,70 +704,70 @@@ return textHeight; } + /** + * Invert the cell color at a position, including both halves of a + * double-width cell. + * + * @param x column position + * @param y row position + */ + public void invertCell(final int x, final int y) { + for (Screen screen: screens) { + screen.invertCell(x, y); + } + } + + /** + * Invert the cell color at a position. + * + * @param x column position + * @param y row position + * @param onlyThisCell if true, only invert this cell, otherwise invert + * both halves of a double-width cell if necessary + */ + public void invertCell(final int x, final int y, + final boolean onlyThisCell) { + + for (Screen screen: screens) { + screen.invertCell(x, y, onlyThisCell); + } + } + + /** + * Set a selection area on the screen. + * + * @param x0 the starting X position of the selection + * @param y0 the starting Y position of the selection + * @param x1 the ending X position of the selection + * @param y1 the ending Y position of the selection + * @param rectangle if true, this is a rectangle select + */ + public void setSelection(final int x0, final int y0, + final int x1, final int y1, final boolean rectangle) { + + for (Screen screen: screens) { + screen.setSelection(x0, y0, x1, y1, rectangle); + } + } + + /** + * Copy the screen selection area to the clipboard. + * + * @param clipboard the clipboard to use + * @param x0 the starting X position of the selection + * @param y0 the starting Y position of the selection + * @param x1 the ending X position of the selection + * @param y1 the ending Y position of the selection + * @param rectangle if true, this is a rectangle select + */ + public void copySelection(final Clipboard clipboard, + final int x0, final int y0, final int x1, final int y1, + final boolean rectangle) { + + // Only copy from the first screen. + if (screens.size() > 0) { + screens.get(0).copySelection(clipboard, x0, y0, x1, y1, rectangle); + } + } + } diff --combined backend/Screen.java index 2a71073,a9a2053..a9a2053 --- a/backend/Screen.java +++ b/backend/Screen.java @@@ -30,6 -30,7 +30,7 @@@ package jexer.backend import jexer.bits.Cell; import jexer.bits.CellAttributes; + import jexer.bits.Clipboard; /** * Drawing operations API. @@@ -409,4 -410,50 +410,50 @@@ public interface Screen */ public int getTextHeight(); + /** + * Invert the cell color at a position, including both halves of a + * double-width cell. + * + * @param x column position + * @param y row position + */ + public void invertCell(final int x, final int y); + + /** + * Invert the cell color at a position. + * + * @param x column position + * @param y row position + * @param onlyThisCell if true, only invert this cell, otherwise invert + * both halves of a double-width cell if necessary + */ + public void invertCell(final int x, final int y, + final boolean onlyThisCell); + + /** + * Set a selection area on the screen. + * + * @param x0 the starting X position of the selection + * @param y0 the starting Y position of the selection + * @param x1 the ending X position of the selection + * @param y1 the ending Y position of the selection + * @param rectangle if true, this is a rectangle select + */ + public void setSelection(final int x0, final int y0, + final int x1, final int y1, final boolean rectangle); + + /** + * Copy the screen selection area to the clipboard. + * + * @param clipboard the clipboard to use + * @param x0 the starting X position of the selection + * @param y0 the starting Y position of the selection + * @param x1 the ending X position of the selection + * @param y1 the ending Y position of the selection + * @param rectangle if true, this is a rectangle select + */ + public void copySelection(final Clipboard clipboard, + final int x0, final int y0, final int x1, final int y1, + final boolean rectangle); + } diff --combined backend/SwingComponent.java index 3d1074c,df36333..df36333 --- a/backend/SwingComponent.java +++ b/backend/SwingComponent.java @@@ -83,7 -83,7 +83,7 @@@ class SwingComponent * Adjustable Insets for this component. This has the effect of adding a * black border around the drawing area. */ - Insets adjustInsets = new Insets(BORDER + 5, BORDER, BORDER, BORDER); + Insets adjustInsets = null; // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- @@@ -96,6 -96,16 +96,16 @@@ */ public SwingComponent(final JFrame frame) { this.frame = frame; + if (System.getProperty("os.name").startsWith("Linux")) { + // On my Linux dev system, a Swing frame draws its contents just + // a little off. No idea why, but I've seen it on both Debian + // and Fedora with KDE. These adjustments to the adjustments + // seem to center it OK in the frame. + adjustInsets = new Insets(BORDER + 5, BORDER, + BORDER - 3, BORDER + 2); + } else { + adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER); + } setupFrame(); } @@@ -106,6 -116,7 +116,7 @@@ */ public SwingComponent(final JComponent component) { this.component = component; + adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER); setupComponent(); } diff --combined backend/SwingTerminal.java index f0ba355,0727efc..0727efc --- a/backend/SwingTerminal.java +++ b/backend/SwingTerminal.java @@@ -36,6 -36,7 +36,7 @@@ import java.awt.Graphics2D import java.awt.Graphics; import java.awt.Insets; import java.awt.Rectangle; + import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; @@@ -578,14 -579,12 +579,12 @@@ public class SwingTerminal extends Logi ) { do { do { - clearPhysical(); drawToSwing(); } while (swing.getBufferStrategy().contentsRestored()); swing.getBufferStrategy().show(); Toolkit.getDefaultToolkit().sync(); } while (swing.getBufferStrategy().contentsLost()); - } else { // Non-triple-buffered, call drawToSwing() once drawToSwing(); @@@ -1238,15 -1237,26 +1237,26 @@@ // Draw the background rectangle, then the foreground character. assert (cell.isImage()); + + // Enable anti-aliasing + if (gr instanceof Graphics2D) { + ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + } + gr.setColor(cell.getBackground()); gr.fillRect(xPixel, yPixel, textWidth, textHeight); BufferedImage image = cell.getImage(); if (image != null) { if (swing.getFrame() != null) { - gr.drawImage(image, xPixel, yPixel, swing.getFrame()); + gr.drawImage(image, xPixel, yPixel, getTextWidth(), + getTextHeight(), swing.getFrame()); } else { - gr.drawImage(image, xPixel, yPixel, swing.getComponent()); + gr.drawImage(image, xPixel, yPixel, getTextWidth(), + getTextHeight(),swing.getComponent()); } return; } @@@ -1308,6 -1318,17 +1318,17 @@@ cellColor.setBackColor(cell.getForeColor()); } + // Enable anti-aliasing + if ((gr instanceof Graphics2D) && (swing.getFrame() != null)) { + // Anti-aliasing on JComponent makes the hash character disappear + // for Terminus font, and also kills performance. Only enable it + // for JFrame. + ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + } + // Draw the background rectangle, then the foreground character. gr2.setColor(attrToBackgroundColor(cellColor)); gr2.fillRect(gr2x, gr2y, textWidth, textHeight); @@@ -1740,13 -1761,16 +1761,16 @@@ } else { ch = key.getKeyChar(); } - alt = key.isAltDown(); + // Both meta and alt count as alt, thanks to Mac using alt for + // "symbols" so meta ("command") is the only other modifier left. + alt = key.isAltDown() | key.isMetaDown(); ctrl = key.isControlDown(); shift = key.isShiftDown(); /* System.err.printf("Swing Key: %s\n", key); System.err.printf(" isKey: %s\n", isKey); + System.err.printf(" meta: %s\n", key.isMetaDown()); System.err.printf(" alt: %s\n", alt); System.err.printf(" ctrl: %s\n", ctrl); System.err.printf(" shift: %s\n", shift); @@@ -2101,6 -2125,10 +2125,10 @@@ boolean eventMouse1 = false; boolean eventMouse2 = false; boolean eventMouse3 = false; + boolean eventAlt = false; + boolean eventCtrl = false; + boolean eventShift = false; + if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { eventMouse1 = true; } @@@ -2110,6 -2138,16 +2138,16 @@@ if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { eventMouse3 = true; } + if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) { + eventAlt = true; + } + if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { + eventCtrl = true; + } + if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) { + eventShift = true; + } + mouse1 = eventMouse1; mouse2 = eventMouse2; mouse3 = eventMouse3; @@@ -2117,7 -2155,8 +2155,8 @@@ int y = textRow(mouse.getY()); TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION, - x, y, x, y, mouse1, mouse2, mouse3, false, false); + x, y, x, y, mouse1, mouse2, mouse3, false, false, + eventAlt, eventCtrl, eventShift); synchronized (eventQueue) { eventQueue.add(mouseEvent); @@@ -2145,8 -2184,24 +2184,24 @@@ oldMouseX = x; oldMouseY = y; + boolean eventAlt = false; + boolean eventCtrl = false; + boolean eventShift = false; + + int modifiers = mouse.getModifiersEx(); + if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) { + eventAlt = true; + } + if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { + eventCtrl = true; + } + if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) { + eventShift = true; + } + TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION, - x, y, x, y, mouse1, mouse2, mouse3, false, false); + x, y, x, y, mouse1, mouse2, mouse3, false, false, + eventAlt, eventCtrl, eventShift); synchronized (eventQueue) { eventQueue.add(mouseEvent); @@@ -2200,6 -2255,10 +2255,10 @@@ boolean eventMouse1 = false; boolean eventMouse2 = false; boolean eventMouse3 = false; + boolean eventAlt = false; + boolean eventCtrl = false; + boolean eventShift = false; + if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { eventMouse1 = true; } @@@ -2209,6 -2268,16 +2268,16 @@@ if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { eventMouse3 = true; } + if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) { + eventAlt = true; + } + if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { + eventCtrl = true; + } + if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) { + eventShift = true; + } + mouse1 = eventMouse1; mouse2 = eventMouse2; mouse3 = eventMouse3; @@@ -2216,7 -2285,8 +2285,8 @@@ int y = textRow(mouse.getY()); TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN, - x, y, x, y, mouse1, mouse2, mouse3, false, false); + x, y, x, y, mouse1, mouse2, mouse3, false, false, + eventAlt, eventCtrl, eventShift); synchronized (eventQueue) { eventQueue.add(mouseEvent); @@@ -2239,6 -2309,10 +2309,10 @@@ boolean eventMouse1 = false; boolean eventMouse2 = false; boolean eventMouse3 = false; + boolean eventAlt = false; + boolean eventCtrl = false; + boolean eventShift = false; + if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { eventMouse1 = true; } @@@ -2248,6 -2322,16 +2322,16 @@@ if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { eventMouse3 = true; } + if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) { + eventAlt = true; + } + if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { + eventCtrl = true; + } + if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) { + eventShift = true; + } + if (mouse1) { mouse1 = false; eventMouse1 = true; @@@ -2264,7 -2348,8 +2348,8 @@@ int y = textRow(mouse.getY()); TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_UP, - x, y, x, y, eventMouse1, eventMouse2, eventMouse3, false, false); + x, y, x, y, eventMouse1, eventMouse2, eventMouse3, false, false, + eventAlt, eventCtrl, eventShift); synchronized (eventQueue) { eventQueue.add(mouseEvent); @@@ -2293,6 -2378,10 +2378,10 @@@ boolean eventMouse3 = false; boolean mouseWheelUp = false; boolean mouseWheelDown = false; + boolean eventAlt = false; + boolean eventCtrl = false; + boolean eventShift = false; + if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) { eventMouse1 = true; } @@@ -2302,6 -2391,16 +2391,16 @@@ if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) { eventMouse3 = true; } + if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) { + eventAlt = true; + } + if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { + eventCtrl = true; + } + if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) { + eventShift = true; + } + mouse1 = eventMouse1; mouse2 = eventMouse2; mouse3 = eventMouse3; @@@ -2315,7 -2414,8 +2414,8 @@@ } TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN, - x, y, x, y, mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown); + x, y, x, y, mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown, + eventAlt, eventCtrl, eventShift); synchronized (eventQueue) { eventQueue.add(mouseEvent); diff --combined bits/Cell.java index a8efa2b,ed3c202..ed3c202 --- a/bits/Cell.java +++ b/bits/Cell.java @@@ -419,7 -419,7 +419,7 @@@ public final class Cell extends CellAtt int B = 23; int hash = A; hash = (B * hash) + super.hashCode(); - hash = (B * hash) + (int)ch; + hash = (B * hash) + ch; hash = (B * hash) + width.hashCode(); if (image != null) { /* diff --combined bits/CellAttributes.java index 99366fd,ad86198..ad86198 --- a/bits/CellAttributes.java +++ b/bits/CellAttributes.java @@@ -62,7 -62,6 +62,6 @@@ public class CellAttributes */ private static final int PROTECT = 0x10; - // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ diff --combined bits/ColorTheme.java index ffba4d4,3efce63..3efce63 --- a/bits/ColorTheme.java +++ b/bits/ColorTheme.java @@@ -178,8 -178,11 +178,11 @@@ public class ColorTheme return; } - while (token.equals("bold") || token.equals("blink")) { - if (token.equals("bold")) { + while (token.equals("bold") + || token.equals("bright") + || token.equals("blink") + ) { + if (token.equals("bold") || token.equals("bright")) { bold = true; token = tokenizer.nextToken(); } @@@ -231,8 -234,8 +234,8 @@@ // Invalid line. continue; } - String key = line.substring(0, line.indexOf(':')).trim(); - String text = line.substring(line.indexOf(':') + 1); + String key = line.substring(0, line.indexOf('=')).trim(); + String text = line.substring(line.indexOf('=') + 1); setColorFromString(key, text); } // All done. @@@ -633,6 -636,11 +636,11 @@@ color.setBackColor(Color.BLUE); color.setBold(false); colors.put("teditor", color); + color = new CellAttributes(); + color.setForeColor(Color.BLACK); + color.setBackColor(Color.CYAN); + color.setBold(false); + colors.put("teditor.selected", color); // TTable color = new CellAttributes(); @@@ -673,6 -681,48 +681,48 @@@ color.setBold(false); colors.put("tsplitpane", color); + // THelpWindow border - during window movement + color = new CellAttributes(); + color.setForeColor(Color.GREEN); + color.setBackColor(Color.CYAN); + color.setBold(true); + colors.put("thelpwindow.windowmove", color); + + // THelpWindow border + color = new CellAttributes(); + color.setForeColor(Color.GREEN); + color.setBackColor(Color.CYAN); + color.setBold(true); + colors.put("thelpwindow.border", color); + + // THelpWindow background + color = new CellAttributes(); + color.setForeColor(Color.WHITE); + color.setBackColor(Color.CYAN); + color.setBold(true); + colors.put("thelpwindow.background", color); + + // THelpWindow text + color = new CellAttributes(); + color.setForeColor(Color.WHITE); + color.setBackColor(Color.BLUE); + color.setBold(false); + colors.put("thelpwindow.text", color); + + // THelpWindow link + color = new CellAttributes(); + color.setForeColor(Color.YELLOW); + color.setBackColor(Color.BLUE); + color.setBold(true); + colors.put("thelpwindow.link", color); + + // THelpWindow link - active + color = new CellAttributes(); + color.setForeColor(Color.YELLOW); + color.setBackColor(Color.CYAN); + color.setBold(true); + colors.put("thelpwindow.link.active", color); + } /** diff --combined bits/StringUtils.java index fffce20,d33f71f..d33f71f --- a/bits/StringUtils.java +++ b/bits/StringUtils.java @@@ -30,6 -30,7 +30,7 @@@ package jexer.bits import java.util.List; import java.util.ArrayList; + import java.util.Arrays; /** * StringUtils contains methods to: @@@ -41,6 -42,11 +42,11 @@@ * * - Read/write a line of RFC4180 comma-separated values strings to/from a * list of strings. + * + * - Compute number of visible text cells for a given Unicode codepoint or + * string. + * + * - Convert bytes to and from base-64 encoding. */ public class StringUtils { @@@ -466,6 -472,10 +472,10 @@@ * @return the number of text cell columns required to display this string */ public static int width(final String str) { + if (str == null) { + return 0; + } + int n = 0; for (int i = 0; i < str.length();) { int ch = str.codePointAt(i); @@@ -495,4 -505,241 +505,241 @@@ return ((ch >= 0x1f004) && (ch <= 0x1fffd)); } + // ------------------------------------------------------------------------ + // Base64 ----------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /* + * The Base64 encoder/decoder below is provided to support JDK 1.6 - JDK + * 11. It was taken from https://sourceforge.net/projects/migbase64/ + * + * The following changes were made: + * + * - Code has been indented and long lines cut to fit within 80 columns. + * + * - Char, String, and "fast" byte functions removed. byte versions + * retained and called toBase64()/fromBase64(). + * + * - Enclosing braces added to blocks. + */ + + /** + * A very fast and memory efficient class to encode and decode to and + * from BASE64 in full accordance with RFC 2045.

    On Windows XP + * sp1 with 1.4.2_04 and later ;), this encoder and decoder is about 10 + * times faster on small arrays (10 - 1000 bytes) and 2-3 times as fast + * on larger arrays (10000 - 1000000 bytes) compared to + * sun.misc.Encoder()/Decoder().

    + * + * On byte arrays the encoder is about 20% faster than Jakarta Commons + * Base64 Codec for encode and about 50% faster for decoding large + * arrays. This implementation is about twice as fast on very small + * arrays (< 30 bytes). If source/destination is a String + * this version is about three times as fast due to the fact that the + * Commons Codec result has to be recoded to a String from + * byte[], which is very expensive.

    + * + * This encode/decode algorithm doesn't create any temporary arrays as + * many other codecs do, it only allocates the resulting array. This + * produces less garbage and it is possible to handle arrays twice as + * large as algorithms that create a temporary array. (E.g. Jakarta + * Commons Codec). It is unknown whether Sun's + * sun.misc.Encoder()/Decoder() produce temporary arrays but + * since performance is quite low it probably does.

    + * + * The encoder produces the same output as the Sun one except that the + * Sun's encoder appends a trailing line separator if the last character + * isn't a pad. Unclear why but it only adds to the length and is + * probably a side effect. Both are in conformance with RFC 2045 + * though.
    Commons codec seem to always att a trailing line + * separator.

    + * + * Note! The encode/decode method pairs (types) come in three + * versions with the exact same algorithm and thus a lot of code + * redundancy. This is to not create any temporary arrays for transcoding + * to/from different format types. The methods not used can simply be + * commented out.

    + * + * There is also a "fast" version of all decode methods that works the + * same way as the normal ones, but har a few demands on the decoded + * input. Normally though, these fast verions should be used if the + * source if the input is known and it hasn't bee tampered with.

    + * + * If you find the code useful or you find a bug, please send me a note + * at base64 @ miginfocom . com. + * + * Licence (BSD): + * ============== + * + * Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (base64 @ miginfocom + * . com) All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the MiG InfoCom AB nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @version 2.2 + * @author Mikael Grev + * Date: 2004-aug-02 + * Time: 11:31:11 + */ + + private static final char[] CA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); + private static final int[] IA = new int[256]; + static { + Arrays.fill(IA, -1); + for (int i = 0, iS = CA.length; i < iS; i++) { + IA[CA[i]] = i; + } + IA['='] = 0; + } + + /** + * Encodes a raw byte array into a BASE64 byte[] + * representation i accordance with RFC 2045. + * @param sArr The bytes to convert. If null or length 0 + * an empty array will be returned. + * @return A BASE64 encoded array. Never null. + */ + public final static String toBase64(byte[] sArr) { + // Check special case + int sLen = sArr != null ? sArr.length : 0; + if (sLen == 0) { + return ""; + } + + final boolean lineSep = true; + + int eLen = (sLen / 3) * 3; // Length of even 24-bits. + int cCnt = ((sLen - 1) / 3 + 1) << 2; // Returned character count + int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0); // Length of returned array + byte[] dArr = new byte[dLen]; + + // Encode even 24-bits + for (int s = 0, d = 0, cc = 0; s < eLen;) { + // Copy next three bytes into lower 24 bits of int, paying + // attension to sign. + int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff); + + // Encode the int into four chars + dArr[d++] = (byte) CA[(i >>> 18) & 0x3f]; + dArr[d++] = (byte) CA[(i >>> 12) & 0x3f]; + dArr[d++] = (byte) CA[(i >>> 6) & 0x3f]; + dArr[d++] = (byte) CA[i & 0x3f]; + + // Add optional line separator + if (lineSep && ++cc == 19 && d < dLen - 2) { + dArr[d++] = '\r'; + dArr[d++] = '\n'; + cc = 0; + } + } + + // Pad and encode last bits if source isn't an even 24 bits. + int left = sLen - eLen; // 0 - 2. + if (left > 0) { + // Prepare the int + int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0); + + // Set last four chars + dArr[dLen - 4] = (byte) CA[i >> 12]; + dArr[dLen - 3] = (byte) CA[(i >>> 6) & 0x3f]; + dArr[dLen - 2] = left == 2 ? (byte) CA[i & 0x3f] : (byte) '='; + dArr[dLen - 1] = '='; + } + try { + return new String(dArr, "UTF-8"); + } catch (java.io.UnsupportedEncodingException e) { + throw new IllegalArgumentException(e); + } + + } + + /** + * Decodes a BASE64 encoded byte array. All illegal characters will + * be ignored and can handle both arrays with and without line + * separators. + * @param sArr The source array. Length 0 will return an empty + * array. null will throw an exception. + * @return The decoded array of bytes. May be of length 0. Will be + * null if the legal characters (including '=') isn't + * divideable by 4. (I.e. definitely corrupted). + */ + public final static byte[] fromBase64(byte[] sArr) { + // Check special case + int sLen = sArr.length; + + // Count illegal characters (including '\r', '\n') to know what + // size the returned array will be, so we don't have to + // reallocate & copy it later. + int sepCnt = 0; // Number of separator characters. (Actually illegal characters, but that's a bonus...) + for (int i = 0; i < sLen; i++) { + // If input is "pure" (I.e. no line separators or illegal chars) + // base64 this loop can be commented out. + if (IA[sArr[i] & 0xff] < 0) { + sepCnt++; + } + } + + // Check so that legal chars (including '=') are evenly + // divideable by 4 as specified in RFC 2045. + if ((sLen - sepCnt) % 4 != 0) { + return null; + } + + int pad = 0; + for (int i = sLen; i > 1 && IA[sArr[--i] & 0xff] <= 0;) { + if (sArr[i] == '=') { + pad++; + } + } + + int len = ((sLen - sepCnt) * 6 >> 3) - pad; + + byte[] dArr = new byte[len]; // Preallocate byte[] of exact length + + for (int s = 0, d = 0; d < len;) { + // Assemble three bytes into an int from four "valid" characters. + int i = 0; + for (int j = 0; j < 4; j++) { // j only increased if a valid char was found. + int c = IA[sArr[s++] & 0xff]; + if (c >= 0) { + i |= c << (18 - j * 6); + } else { + j--; + } + } + + // Add the bytes + dArr[d++] = (byte) (i >> 16); + if (d < len) { + dArr[d++]= (byte) (i >> 8); + if (d < len) { + dArr[d++] = (byte) i; + } + } + } + + return dArr; + } + } diff --combined demos/Demo6.java index db0b5c9,41d1f2c..41d1f2c --- a/demos/Demo6.java +++ b/demos/Demo6.java @@@ -111,7 -111,7 +111,7 @@@ public class Demo6 * Make a new Swing window for the second application. */ SwingBackend monitorBackend = new SwingBackend(width + 5, - height + 5, 16); + height + 5, 20); /* * Setup the second application, give it the basic file and diff --combined demos/DemoCheckBoxWindow.java index fda7bd7,faf3530..faf3530 --- a/demos/DemoCheckBoxWindow.java +++ b/demos/DemoCheckBoxWindow.java @@@ -103,8 -103,9 +103,9 @@@ public class DemoCheckBoxWindow extend TRadioGroup group = addRadioGroup(1, row, i18n.getString("radioGroupTitle")); group.addRadioButton(i18n.getString("radioOption1")); - group.addRadioButton(i18n.getString("radioOption2")); + group.addRadioButton(i18n.getString("radioOption2"), true); group.addRadioButton(i18n.getString("radioOption3")); + group.setRequiresSelection(true); List comboValues = new ArrayList(); comboValues.add(i18n.getString("comboBoxString0")); diff --combined event/TMouseEvent.java index 496d8bc,e529898..e529898 --- a/event/TMouseEvent.java +++ b/event/TMouseEvent.java @@@ -118,6 -118,21 +118,21 @@@ public class TMouseEvent extends TInput */ private boolean mouseWheelDown; + /** + * Keyboard modifier ALT. + */ + private boolean alt; + + /** + * Keyboard modifier CTRL. + */ + private boolean ctrl; + + /** + * Keyboard modifier SHIFT. + */ + private boolean shift; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@@ -135,11 -150,15 +150,15 @@@ * @param mouse3 if true, middle button is down * @param mouseWheelUp if true, mouse wheel (button 4) is down * @param mouseWheelDown if true, mouse wheel (button 5) is down + * @param alt if true, ALT was pressed with this mouse event + * @param ctrl if true, CTRL was pressed with this mouse event + * @param shift if true, SHIFT was pressed with this mouse event */ public TMouseEvent(final Type type, final int x, final int y, final int absoluteX, final int absoluteY, final boolean mouse1, final boolean mouse2, final boolean mouse3, - final boolean mouseWheelUp, final boolean mouseWheelDown) { + final boolean mouseWheelUp, final boolean mouseWheelDown, + final boolean alt, final boolean ctrl, final boolean shift) { this.type = type; this.x = x; @@@ -151,6 -170,9 +170,9 @@@ this.mouse3 = mouse3; this.mouseWheelUp = mouseWheelUp; this.mouseWheelDown = mouseWheelDown; + this.alt = alt; + this.ctrl = ctrl; + this.shift = shift; } // ------------------------------------------------------------------------ @@@ -289,6 -311,33 +311,33 @@@ return mouseWheelDown; } + /** + * Getter for ALT. + * + * @return alt value + */ + public boolean isAlt() { + return alt; + } + + /** + * Getter for CTRL. + * + * @return ctrl value + */ + public boolean isCtrl() { + return ctrl; + } + + /** + * Getter for SHIFT. + * + * @return shift value + */ + public boolean isShift() { + return shift; + } + /** * Create a duplicate instance. * @@@ -296,7 -345,9 +345,9 @@@ */ public TMouseEvent dup() { TMouseEvent mouse = new TMouseEvent(type, x, y, absoluteX, absoluteY, - mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown); + mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown, + alt, ctrl, shift); + return mouse; } @@@ -307,7 -358,7 +358,7 @@@ */ @Override public String toString() { - return String.format("Mouse: %s x %d y %d absoluteX %d absoluteY %d 1 %s 2 %s 3 %s DOWN %s UP %s", + return String.format("Mouse: %s x %d y %d absoluteX %d absoluteY %d 1 %s 2 %s 3 %s DOWN %s UP %s ALT %s CTRL %s SHIFT %s", type, x, y, absoluteX, absoluteY, @@@ -315,7 -366,8 +366,8 @@@ mouse2, mouse3, mouseWheelUp, - mouseWheelDown); + mouseWheelDown, + alt, ctrl, shift); } } diff --combined io/TimeoutInputStream.java index 3d8cdb0,70faff4..70faff4 --- a/io/TimeoutInputStream.java +++ b/io/TimeoutInputStream.java @@@ -244,7 -244,7 +244,7 @@@ public class TimeoutInputStream extend if (timeoutMillis == 0) { // Block on the read(). - return stream.read(b); + return stream.read(b, off, len); } int remaining = len; diff --combined layout/StretchLayoutManager.java index ee2bf5a,4bcb0cf..4bcb0cf --- a/layout/StretchLayoutManager.java +++ b/layout/StretchLayoutManager.java @@@ -146,11 -146,11 +146,11 @@@ public class StretchLayoutManager imple */ private void layoutChildren() { double widthRatio = (double) width / originalWidth; - if (!Double.isFinite(widthRatio)) { + if (Math.abs(widthRatio) > Double.MAX_VALUE) { widthRatio = 1; } double heightRatio = (double) height / originalHeight; - if (!Double.isFinite(heightRatio)) { + if (Math.abs(heightRatio) > Double.MAX_VALUE) { heightRatio = 1; } for (TWidget child: children.keySet()) { diff --combined menu/TMenu.java index 6d746df,6a875c7..6a875c7 --- a/menu/TMenu.java +++ b/menu/TMenu.java @@@ -72,10 -72,12 +72,12 @@@ public class TMenu extends TWindow public static final int MID_SHELL = 13; // Edit menu - public static final int MID_CUT = 20; - public static final int MID_COPY = 21; - public static final int MID_PASTE = 22; - public static final int MID_CLEAR = 23; + public static final int MID_UNDO = 20; + public static final int MID_REDO = 21; + public static final int MID_CUT = 22; + public static final int MID_COPY = 23; + public static final int MID_PASTE = 24; + public static final int MID_CLEAR = 25; // Search menu public static final int MID_FIND = 30; @@@ -152,6 -154,11 +154,11 @@@ */ private MnemonicString mnemonic; + /** + * If true, draw icons with menu items. Note package private access. + */ + boolean useIcons = false; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@@ -182,6 -189,11 +189,11 @@@ setHeight(2); setActive(false); + + if (System.getProperty("jexer.menuIcons", "false").equals("true")) { + useIcons = true; + } + } // ------------------------------------------------------------------------ @@@ -446,7 -458,7 +458,7 @@@ final boolean enabled) { assert (id >= 1024); - return addItemInternal(id, label, null, enabled); + return addItemInternal(id, label, null, enabled, -1); } /** @@@ -492,7 -504,7 +504,7 @@@ private TMenuItem addItemInternal(final int id, final String label, final TKeypress key) { - return addItemInternal(id, label, key, true); + return addItemInternal(id, label, key, true, -1); } /** @@@ -502,15 -514,16 +514,16 @@@ * @param label menu item label * @param key global keyboard accelerator * @param enabled default state for enabled + * @param icon icon picture/emoji * @return the new menu item */ private TMenuItem addItemInternal(final int id, final String label, - final TKeypress key, final boolean enabled) { + final TKeypress key, final boolean enabled, final int icon) { int newY = getChildren().size() + 1; assert (newY < getHeight()); - TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label); + TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label, icon); menuItem.setKey(key); menuItem.setEnabled(enabled); setHeight(getHeight() + 1); @@@ -551,6 -564,7 +564,7 @@@ String label; TKeypress key = null; + int icon = -1; boolean checkable = false; boolean checked = false; @@@ -558,6 -572,7 +572,7 @@@ case MID_REPAINT: label = i18n.getString("menuRepaintDesktop"); + icon = 0x1F3A8; break; case MID_VIEW_IMAGE: @@@ -570,41 -585,56 +585,56 @@@ case MID_NEW: label = i18n.getString("menuNew"); + icon = 0x1F5CE; break; case MID_EXIT: label = i18n.getString("menuExit"); key = kbAltX; + icon = 0x1F5D9; break; case MID_SHELL: label = i18n.getString("menuShell"); + icon = 0x1F5AE; break; case MID_OPEN_FILE: label = i18n.getString("menuOpen"); key = kbF3; + icon = 0x1F5C1; break; + case MID_UNDO: + label = i18n.getString("menuUndo"); + key = kbCtrlZ; + break; + case MID_REDO: + label = i18n.getString("menuRedo"); + key = kbCtrlY; + break; case MID_CUT: label = i18n.getString("menuCut"); key = kbCtrlX; + icon = 0x1F5F6; break; case MID_COPY: label = i18n.getString("menuCopy"); key = kbCtrlC; + icon = 0x1F5D0; break; case MID_PASTE: label = i18n.getString("menuPaste"); key = kbCtrlV; + icon = 0x1F4CB; break; case MID_CLEAR: label = i18n.getString("menuClear"); - // key = kbDel; break; case MID_FIND: label = i18n.getString("menuFind"); + icon = 0x1F50D; break; case MID_REPLACE: label = i18n.getString("menuReplace"); @@@ -622,6 -652,7 +652,7 @@@ break; case MID_CASCADE: label = i18n.getString("menuWindowCascade"); + icon = 0x1F5D7; break; case MID_CLOSE_ALL: label = i18n.getString("menuWindowCloseAll"); @@@ -629,18 -660,22 +660,22 @@@ case MID_WINDOW_MOVE: label = i18n.getString("menuWindowMove"); key = kbCtrlF5; + icon = 0x263C; break; case MID_WINDOW_ZOOM: label = i18n.getString("menuWindowZoom"); key = kbF5; + icon = 0x2195; break; case MID_WINDOW_NEXT: label = i18n.getString("menuWindowNext"); key = kbF6; + icon = 0x2192; break; case MID_WINDOW_PREVIOUS: label = i18n.getString("menuWindowPrevious"); key = kbShiftF6; + icon = 0x2190; break; case MID_WINDOW_CLOSE: label = i18n.getString("menuWindowClose"); @@@ -775,7 -810,7 +810,7 @@@ throw new IllegalArgumentException("Invalid menu ID: " + id); } - TMenuItem item = addItemInternal(id, label, key, enabled); + TMenuItem item = addItemInternal(id, label, key, enabled, icon); item.setCheckable(checkable); return item; } diff --combined menu/TMenu.properties index 4a0f8e6,692293e..692293e --- a/menu/TMenu.properties +++ b/menu/TMenu.properties @@@ -2,6 -2,8 +2,8 @@@ menuNew=&Ne menuExit=E&xit menuShell=O&S Shell menuOpen=&Open + menuUndo=&Undo + menuRedo=&Redo menuCut=Cu&t menuCopy=&Copy menuPaste=&Paste diff --combined menu/TMenuItem.java index d9dfc2a,b478059..b478059 --- a/menu/TMenuItem.java +++ b/menu/TMenuItem.java @@@ -80,6 -80,11 +80,11 @@@ public class TMenuItem extends TWidget */ private MnemonicString mnemonic; + /** + * An optional 2-cell-wide picture/icon for this item. + */ + private int icon = -1; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@@ -96,6 -101,22 +101,22 @@@ TMenuItem(final TMenu parent, final int id, final int x, final int y, final String label) { + this(parent, id, x, y, label, -1); + } + + /** + * Package private constructor. + * + * @param parent parent widget + * @param id menu id + * @param x column relative to parent + * @param y row relative to parent + * @param label menu item title + * @param icon icon picture/emoji + */ + TMenuItem(final TMenu parent, final int id, final int x, final int y, + final String label, final int icon) { + // Set parent and window super(parent); @@@ -105,8 -126,13 +126,13 @@@ setY(y); setHeight(1); this.label = mnemonic.getRawLabel(); - setWidth(StringUtils.width(label) + 4); + if (parent.useIcons) { + setWidth(StringUtils.width(label) + 6); + } else { + setWidth(StringUtils.width(label) + 4); + } this.id = id; + this.icon = icon; // Default state for some known menu items switch (id) { @@@ -220,26 -246,31 +246,31 @@@ } } + boolean useIcons = ((TMenu) getParent()).useIcons; + char cVSide = GraphicsChars.WINDOW_SIDE; vLineXY(0, 0, 1, cVSide, background); vLineXY(getWidth() - 1, 0, 1, cVSide, background); hLineXY(1, 0, getWidth() - 2, ' ', menuColor); - putStringXY(2, 0, mnemonic.getRawLabel(), menuColor); + putStringXY(2 + (useIcons ? 2 : 0), 0, mnemonic.getRawLabel(), + menuColor); if (key != null) { String keyLabel = key.toString(); putStringXY((getWidth() - StringUtils.width(keyLabel) - 2), 0, keyLabel, menuColor); } if (mnemonic.getScreenShortcutIdx() >= 0) { - putCharXY(2 + mnemonic.getScreenShortcutIdx(), 0, - mnemonic.getShortcut(), menuMnemonicColor); + putCharXY(2 + (useIcons ? 2 : 0) + mnemonic.getScreenShortcutIdx(), + 0, mnemonic.getShortcut(), menuMnemonicColor); } if (checked) { assert (checkable); putCharXY(1, 0, GraphicsChars.CHECK, menuColor); } - + if ((useIcons == true) && (icon != -1)) { + putCharXY(2, 0, icon, menuColor); + } } // ------------------------------------------------------------------------ @@@ -318,12 -349,34 +349,34 @@@ if (key != null) { int newWidth = (StringUtils.width(label) + 4 + StringUtils.width(key.toString()) + 2); + if (((TMenu) getParent()).useIcons) { + newWidth += 2; + } if (newWidth > getWidth()) { setWidth(newWidth); } } } + /** + * Get a picture/emoji icon for this menu item. + * + * @return the codepoint, or -1 if no icon is specified for this menu + * item + */ + public final int getIcon() { + return icon; + } + + /** + * Set a picture/emoji icon for this menu item. + * + * @param icon a codepoint, or -1 to unset the icon + */ + public final void setIcon(final int icon) { + this.icon = icon; + } + /** * Dispatch event(s) due to selection or click. */ diff --combined menu/TSubMenu.java index e285c5a,be281b5..be281b5 --- a/menu/TSubMenu.java +++ b/menu/TSubMenu.java @@@ -212,6 -212,21 +212,21 @@@ public class TSubMenu extends TMenuIte return menu.addItem(id, label, key); } + /** + * Convenience function to add a custom menu item. + * + * @param id menu item ID. Must be greater than 1024. + * @param label menu item label + * @param key global keyboard accelerator + * @param enabled default state for enabled + * @return the new menu item + */ + public TMenuItem addItem(final int id, final String label, + final TKeypress key, final boolean enabled) { + + return menu.addItem(id, label, key, enabled); + } + /** * Convenience function to add a menu item. * @@@ -223,6 -238,20 +238,20 @@@ return menu.addItem(id, label); } + /** + * Convenience function to add a menu item. + * + * @param id menu item ID. Must be greater than 1024. + * @param label menu item label + * @param enabled default state for enabled + * @return the new menu item + */ + public TMenuItem addItem(final int id, final String label, + final boolean enabled) { + + return menu.addItem(id, label, enabled); + } + /** * Convenience function to add one of the default menu items. * diff --combined teditor/Document.java index 2abfef6,b4a9a3b..b4a9a3b --- a/teditor/Document.java +++ b/teditor/Document.java @@@ -76,6 -76,23 +76,23 @@@ public class Document */ private Highlighter highlighter = new Highlighter(); + /** + * The tab stop size. + */ + private int tabSize = 8; + + /** + * If true, backspace at an indent level goes back a full indent level. + * If false, backspace always goes back one column. + */ + private boolean backspaceUnindents = false; + + /** + * If true, save files with tab characters. If false, convert tabs to + * spaces when saving files. + */ + private boolean saveWithTabs = false; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@@ -89,7 -106,8 +106,8 @@@ public Document(final String str, final CellAttributes defaultColor) { this.defaultColor = defaultColor; - // TODO: set different colors based on file extension + // Set colors to resemble the Borland IDE colors, but for Java + // language keywords. highlighter.setJavaColors(); String [] rawLines = str.split("\n"); @@@ -98,16 -116,41 +116,41 @@@ } } + /** + * Private constructor used by dup(). + */ + private Document() { + // NOP + } + // ------------------------------------------------------------------------ // Document --------------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Create a duplicate instance. + * + * @return duplicate intance + */ + public Document dup() { + Document other = new Document(); + for (Line line: lines) { + other.lines.add(line.dup()); + } + other.lineNumber = lineNumber; + other.overwrite = overwrite; + other.dirty = dirty; + other.defaultColor = defaultColor; + other.highlighter.setTo(highlighter); + return other; + } + /** * Get the overwrite flag. * * @return true if addChar() overwrites data, false if it inserts */ - public boolean getOverwrite() { + public boolean isOverwrite() { return overwrite; } @@@ -120,6 -163,13 +163,13 @@@ return dirty; } + /** + * Unset the dirty flag. + */ + public void setNotDirty() { + dirty = false; + } + /** * Save contents to file. * @@@ -133,7 -183,11 +183,11 @@@ "UTF-8"); for (Line line: lines) { - output.write(line.getRawString()); + if (saveWithTabs) { + output.write(convertSpacesToTabs(line.getRawString())); + } else { + output.write(line.getRawString()); + } output.write("\n"); } @@@ -362,7 -416,7 +416,7 @@@ // If at the beginning of a word already, push past it. if ((getChar() != -1) && (getRawLine().length() > 0) - && !Character.isSpace((char) getChar()) + && !Character.isWhitespace((char) getChar()) ) { left(); } @@@ -370,7 -424,7 +424,7 @@@ // int line = lineNumber; while ((getChar() == -1) || (getRawLine().length() == 0) - || Character.isSpace((char) getChar()) + || Character.isWhitespace((char) getChar()) ) { if (left() == false) { return; @@@ -380,12 -434,12 +434,12 @@@ assert (getChar() != -1); - if (!Character.isSpace((char) getChar()) + if (!Character.isWhitespace((char) getChar()) && (getRawLine().length() > 0) ) { // Advance until at the beginning of the document or a whitespace // is encountered. - while (!Character.isSpace((char) getChar())) { + while (!Character.isWhitespace((char) getChar())) { int line = lineNumber; if (left() == false) { // End of document, bail out. @@@ -418,7 -472,7 +472,7 @@@ } if (lineNumber != line) { // We wrapped a line. Here that counts as whitespace. - if (!Character.isSpace((char) getChar())) { + if (!Character.isWhitespace((char) getChar())) { // We found a character immediately after the line. // Done! return; @@@ -429,12 -483,12 +483,12 @@@ } assert (getChar() != -1); - if (!Character.isSpace((char) getChar()) + if (!Character.isWhitespace((char) getChar()) && (getRawLine().length() > 0) ) { // Advance until at the end of the document or a whitespace is // encountered. - while (!Character.isSpace((char) getChar())) { + while (!Character.isWhitespace((char) getChar())) { line = lineNumber; if (right() == false) { // End of document, bail out. @@@ -442,7 -496,7 +496,7 @@@ } if (lineNumber != line) { // We wrapped a line. Here that counts as whitespace. - if (!Character.isSpace((char) getChar()) + if (!Character.isWhitespace((char) getChar()) && (getRawLine().length() > 0) ) { // We found a character immediately after the line. @@@ -462,7 -516,7 +516,7 @@@ } if (lineNumber != line) { // We wrapped a line. Here that counts as whitespace. - if (!Character.isSpace((char) getChar())) { + if (!Character.isWhitespace((char) getChar())) { // We found a character immediately after the line. // Done! return; @@@ -473,10 -527,10 +527,10 @@@ } assert (getChar() != -1); - if (Character.isSpace((char) getChar())) { + if (Character.isWhitespace((char) getChar())) { // Advance until at the end of the document or a non-whitespace // is encountered. - while (Character.isSpace((char) getChar())) { + while (Character.isWhitespace((char) getChar())) { if (right() == false) { // End of document, bail out. return; @@@ -543,7 -597,7 +597,7 @@@ dirty = true; int cursor = lines.get(lineNumber).getCursor(); if (cursor > 0) { - lines.get(lineNumber).backspace(); + lines.get(lineNumber).backspace(tabSize, backspaceUnindents); } else if (lineNumber > 0) { // Join two lines lineNumber--; @@@ -595,6 -649,62 +649,62 @@@ } } + /** + * Get the tab stop size. + * + * @return the tab stop size + */ + public int getTabSize() { + return tabSize; + } + + /** + * Set the tab stop size. + * + * @param tabSize the new tab stop size + */ + public void setTabSize(final int tabSize) { + this.tabSize = tabSize; + } + + /** + * Set the backspace unindent option. + * + * @param backspaceUnindents If true, backspace at an indent level goes + * back a full indent level. If false, backspace always goes back one + * column. + */ + public void setBackspaceUnindents(final boolean backspaceUnindents) { + this.backspaceUnindents = backspaceUnindents; + } + + /** + * Set the save with tabs option. + * + * @param saveWithTabs If true, save files with tab characters. If + * false, convert tabs to spaces when saving files. + */ + public void setSaveWithTabs(final boolean saveWithTabs) { + this.saveWithTabs = saveWithTabs; + } + + /** + * Handle the tab character. + */ + public void tab() { + if (overwrite) { + del(); + } + lines.get(lineNumber).tab(tabSize); + } + + /** + * Handle the backtab (shift-tab) character. + */ + public void backTab() { + lines.get(lineNumber).backTab(tabSize); + } + /** * Get a (shallow) copy of the list of lines. * @@@ -637,4 -747,77 +747,77 @@@ return lines.get(lineNumber).getDisplayLength(); } + /** + * Get the entire contents of the document as one string. + * + * @return the document contents + */ + public String getText() { + StringBuilder sb = new StringBuilder(); + for (Line line: getLines()) { + sb.append(line.getRawString()); + sb.append("\n"); + } + return sb.toString(); + } + + /** + * Trim trailing whitespace from lines and trailing empty + * lines from the document. + */ + public void cleanWhitespace() { + for (Line line: getLines()) { + line.trimRight(); + } + if (lines.size() == 0) { + return; + } + while (lines.get(lines.size() - 1).length() == 0) { + lines.remove(lines.size() - 1); + } + if (lineNumber > lines.size() - 1) { + lineNumber = lines.size() - 1; + } + } + + /** + * Set keyword highlighting. + * + * @param enabled if true, enable keyword highlighting + */ + public void setHighlighting(final boolean enabled) { + highlighter.setEnabled(enabled); + for (Line line: getLines()) { + line.scanLine(); + } + } + + /** + * Convert a string with leading spaces to a mix of tabs and spaces. + * + * @param string the string to convert + */ + private String convertSpacesToTabs(final String string) { + if (string.length() == 0) { + return string; + } + + int start = 0; + while (string.charAt(start) == ' ') { + start++; + } + int tabCount = start / 8; + if (tabCount == 0) { + return string; + } + + StringBuilder sb = new StringBuilder(string.length()); + + for (int i = 0; i < tabCount; i++) { + sb.append('\t'); + } + sb.append(string.substring(tabCount * 8)); + return sb.toString(); + } + } diff --combined teditor/Highlighter.java index a484194,23ee900..23ee900 --- a/teditor/Highlighter.java +++ b/teditor/Highlighter.java @@@ -56,13 -56,36 +56,36 @@@ public class Highlighter * Public constructor sets the theme to the default. */ public Highlighter() { - colors = new TreeMap(); + // NOP } // ------------------------------------------------------------------------ // Highlighter ------------------------------------------------------------ // ------------------------------------------------------------------------ + /** + * Set keyword highlighting. + * + * @param enabled if true, enable keyword highlighting + */ + public void setEnabled(final boolean enabled) { + if (enabled) { + setJavaColors(); + } else { + colors = null; + } + } + + /** + * Set my field values to that's field. + * + * @param rhs an instance of Highlighter + */ + public void setTo(final Highlighter rhs) { + colors = new TreeMap(); + colors.putAll(rhs.colors); + } + /** * See if this is a character that should split a word. * @@@ -87,7 -110,10 +110,10 @@@ * @return color associated with name, e.g. bold yellow on blue */ public CellAttributes getColor(final String name) { - CellAttributes attr = (CellAttributes) colors.get(name); + if (colors == null) { + return null; + } + CellAttributes attr = colors.get(name); return attr; } @@@ -95,19 -121,41 +121,41 @@@ * Sets to defaults that resemble the Borland IDE colors. */ public void setJavaColors() { + colors = new TreeMap(); + CellAttributes color; - String [] keywords = { + String [] types = { "boolean", "byte", "short", "int", "long", "char", "float", - "double", "void", "new", - "static", "final", "volatile", "synchronized", "abstract", - "public", "private", "protected", - "class", "interface", "extends", "implements", + "double", "void", + }; + color = new CellAttributes(); + color.setForeColor(Color.GREEN); + color.setBackColor(Color.BLUE); + color.setBold(true); + for (String str: types) { + colors.put(str, color); + } + + String [] modifiers = { + "abstract", "final", "native", "private", "protected", "public", + "static", "strictfp", "synchronized", "transient", "volatile", + }; + color = new CellAttributes(); + color.setForeColor(Color.WHITE); + color.setBackColor(Color.BLUE); + color.setBold(true); + for (String str: modifiers) { + colors.put(str, color); + } + + String [] keywords = { + "new", "class", "interface", "extends", "implements", "if", "else", "do", "while", "for", "break", "continue", "switch", "case", "default", }; color = new CellAttributes(); - color.setForeColor(Color.WHITE); + color.setForeColor(Color.YELLOW); color.setBackColor(Color.BLUE); color.setBold(true); for (String str: keywords) { diff --combined teditor/Line.java index 7cd5feb,b5c980a..b5c980a --- a/teditor/Line.java +++ b/teditor/Line.java @@@ -32,6 -32,7 +32,7 @@@ import java.util.ArrayList import java.util.List; import jexer.bits.CellAttributes; + import jexer.bits.GraphicsChars; import jexer.bits.StringUtils; /** @@@ -92,7 -93,31 +93,31 @@@ public class Line this.defaultColor = defaultColor; this.highlighter = highlighter; - this.rawText = new StringBuilder(str); + + this.rawText = new StringBuilder(); + int col = 0; + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + if (ch == '\t') { + // Expand tabs + int j = col % 8; + do { + rawText.append(' '); + j++; + col++; + } while ((j % 8) != 0); + continue; + } + if ((ch <= 0x20) || (ch == 0x7F)) { + // Replace all other C0 bytes with CP437 glyphs. + rawText.append(GraphicsChars.CP437[(int) ch]); + col++; + continue; + } + + rawText.append(ch); + col++; + } scanLine(); } @@@ -107,10 -132,33 +132,33 @@@ this(str, defaultColor, null); } + /** + * Private constructor used by dup(). + */ + private Line() { + // NOP + } + // ------------------------------------------------------------------------ // Line ------------------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Create a duplicate instance. + * + * @return duplicate intance + */ + public Line dup() { + Line other = new Line(); + other.defaultColor = defaultColor; + other.highlighter = highlighter; + other.position = position; + other.screenPosition = screenPosition; + other.rawText = new StringBuilder(rawText); + other.scanLine(); + return other; + } + /** * Get a (shallow) copy of the words in this line. * @@@ -193,9 -241,19 +241,19 @@@ } /** - * Scan rawText and make words out of it. + * Get the raw length of this line. + * + * @return the length of this line in characters, which may be different + * from the number of cells needed to display it + */ + public int length() { + return rawText.length(); + } + + /** + * Scan rawText and make words out of it. Note package private access. */ - private void scanLine() { + void scanLine() { words.clear(); Word word = new Word(this.defaultColor, this.highlighter); words.add(word); @@@ -236,7 -294,7 +294,7 @@@ if (getDisplayLength() == 0) { return false; } - if (position == getDisplayLength() - 1) { + if (screenPosition == getDisplayLength() - 1) { return false; } if (position < rawText.length()) { @@@ -267,7 -325,7 +325,7 @@@ * @return true if the cursor position changed */ public boolean end() { - if (position != getDisplayLength() - 1) { + if (screenPosition != getDisplayLength() - 1) { position = rawText.length(); screenPosition = StringUtils.width(rawText.toString()); return true; @@@ -281,7 -339,7 +339,7 @@@ public void del() { assert (words.size() > 0); - if (position < getDisplayLength()) { + if (screenPosition < getDisplayLength()) { int n = Character.charCount(rawText.codePointAt(position)); for (int i = 0; i < n; i++) { rawText.deleteCharAt(position); @@@ -294,8 -352,32 +352,32 @@@ /** * Delete the character immediately preceeding the cursor. + * + * @param tabSize the tab stop size + * @param backspaceUnindents If true, backspace at an indent level goes + * back a full indent level. If false, backspace always goes back one + * column. */ - public void backspace() { + public void backspace(final int tabSize, final boolean backspaceUnindents) { + if ((backspaceUnindents == true) + && (tabSize > 0) + && (screenPosition > 0) + && (rawText.charAt(position - 1) == ' ') + && ((screenPosition % tabSize) == 0) + ) { + boolean doBackTab = true; + for (int i = 0; i < position; i++) { + if (rawText.charAt(i) != ' ') { + doBackTab = false; + break; + } + } + if (doBackTab) { + backTab(tabSize); + return; + } + } + if (left()) { del(); } @@@ -307,7 -389,7 +389,7 @@@ * @param ch the character to insert */ public void addChar(final int ch) { - if (position < getDisplayLength() - 1) { + if (screenPosition < getDisplayLength() - 1) { rawText.insert(position, Character.toChars(ch)); } else { rawText.append(Character.toChars(ch)); @@@ -323,7 -405,7 +405,7 @@@ * @param ch the character to replace */ public void replaceChar(final int ch) { - if (position < getDisplayLength() - 1) { + if (screenPosition < getDisplayLength() - 1) { // Replace character String oldText = rawText.toString(); rawText = new StringBuilder(oldText.substring(0, position)); @@@ -345,7 -427,7 +427,7 @@@ * @param screenPosition the position on screen * @return the equivalent position in text */ - protected int screenToTextPosition(final int screenPosition) { + private int screenToTextPosition(final int screenPosition) { if (screenPosition == 0) { return 0; } @@@ -362,4 -444,55 +444,55 @@@ " exceeds available text length " + rawText.length()); } + /** + * Trim trailing whitespace from line, repositioning cursor if needed. + */ + public void trimRight() { + if (rawText.length() == 0) { + return; + } + if (!Character.isWhitespace(rawText.charAt(rawText.length() - 1))) { + return; + } + while ((rawText.length() > 0) + && Character.isWhitespace(rawText.charAt(rawText.length() - 1)) + ) { + rawText.deleteCharAt(rawText.length() - 1); + } + if (position >= rawText.length()) { + end(); + } + scanLine(); + } + + /** + * Handle the tab character. + * + * @param tabSize the tab stop size + */ + public void tab(final int tabSize) { + if (tabSize > 0) { + do { + addChar(' '); + } while ((screenPosition % tabSize) != 0); + } + } + + /** + * Handle the backtab (shift-tab) character. + * + * @param tabSize the tab stop size + */ + public void backTab(final int tabSize) { + if ((tabSize > 0) && (screenPosition > 0) + && (rawText.charAt(position - 1) == ' ') + ) { + do { + backspace(tabSize, false); + } while (((screenPosition % tabSize) != 0) + && (screenPosition > 0) + && (rawText.charAt(position - 1) == ' ')); + } + } + } diff --combined teditor/Word.java index eada29c,483f9c3..483f9c3 --- a/teditor/Word.java +++ b/teditor/Word.java @@@ -135,11 -135,6 +135,6 @@@ public class Word * @return the number of cells needed to display this word */ public int getDisplayLength() { - // For now, just use the text length. In the future, this will be a - // grapheme count. - - // TODO: figure out how to handle the tab character. Do we have a - // global tab stops list and current word position? return StringUtils.width(text.toString()); } diff --combined tterminal/DisplayLine.java index 06a05a3,87e6952..87e6952 --- a/tterminal/DisplayLine.java +++ b/tterminal/DisplayLine.java @@@ -248,4 -248,29 +248,29 @@@ public class DisplayLine chars[chars.length - 1] = new Cell(newCell); } + /** + * Determine if line contains image data. + * + * @return true if the line has image data + */ + public boolean isImage() { + for (int i = 0; i < chars.length; i++) { + if (chars[i].isImage()) { + return true; + } + } + return false; + } + + /** + * Clear image data from line. + */ + public void clearImages() { + for (int i = 0; i < chars.length; i++) { + if (chars[i].isImage()) { + chars[i].reset(); + } + } + } + } diff --combined tterminal/ECMA48.java index 1d34811,537b2e0..537b2e0 --- a/tterminal/ECMA48.java +++ b/tterminal/ECMA48.java @@@ -28,9 -28,11 +28,11 @@@ */ package jexer.tterminal; - import java.awt.Graphics2D; + import java.awt.Graphics; import java.awt.image.BufferedImage; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; + import java.io.ByteArrayInputStream; import java.io.CharArrayWriter; import java.io.InputStream; import java.io.InputStreamReader; @@@ -45,6 -47,7 +47,7 @@@ import java.util.ArrayList import java.util.Collections; import java.util.HashMap; import java.util.List; + import javax.imageio.ImageIO; import jexer.TKeypress; import jexer.backend.GlyphMaker; @@@ -256,7 -259,7 +259,7 @@@ public class ECMA48 implements Runnabl /** * The type of emulator to be. */ - private DeviceType type = DeviceType.VT102; + private final DeviceType type; /** * The scrollback buffer characters + attributes. @@@ -271,7 -274,7 +274,7 @@@ /** * The maximum number of lines in the scrollback buffer. */ - private int maxScrollback = 10000; + private int scrollbackMax = 10000; /** * The terminal's input. For type == XTERM, this is an InputStreamReader @@@ -323,29 -326,29 +326,29 @@@ * Physical display width. We start at 80x24, but the user can resize us * bigger/smaller. */ - private int width; + private int width = 80; /** * Physical display height. We start at 80x24, but the user can resize * us bigger/smaller. */ - private int height; + private int height = 24; /** * Top margin of the scrolling region. */ - private int scrollRegionTop; + private int scrollRegionTop = 0; /** * Bottom margin of the scrolling region. */ - private int scrollRegionBottom; + private int scrollRegionBottom = height - 1; /** * Right margin column number. This can be selected by the remote side * to be 80/132 (rightMargin values 79/131), or it can be (width - 1). */ - private int rightMargin; + private int rightMargin = 79; /** * Last character printed. @@@ -357,7 -360,7 +360,7 @@@ * 132), but the line does NOT wrap until another character is written to * column 1 of the next line, after which the cursor moves to column 2. */ - private boolean wrapLineFlag; + private boolean wrapLineFlag = false; /** * VT220 single shift flag. @@@ -394,7 -397,7 +397,7 @@@ /** * Non-csi collect buffer. */ - private StringBuilder collectBuffer; + private StringBuilder collectBuffer = new StringBuilder(128); /** * When true, use the G1 character set. @@@ -469,7 -472,7 +472,7 @@@ /** * Sixel collection buffer. */ - private StringBuilder sixelParseBuffer; + private StringBuilder sixelParseBuffer = new StringBuilder(2048); /** * Sixel shared palette. @@@ -503,6 -506,11 +506,11 @@@ */ private ArrayList userQueue = new ArrayList(); + /** + * Number of bytes/characters passed to consume(). + */ + private long readCount = 0; + /** * DECSC/DECRC save/restore a subset of the total state. This class * encapsulates those specific flags/modes. @@@ -655,7 -663,8 +663,8 @@@ this.inputStream = new TimeoutInputStream(inputStream, 2000); } if (type == DeviceType.XTERM) { - this.input = new InputStreamReader(this.inputStream, "UTF-8"); + this.input = new InputStreamReader(new BufferedInputStream( + this.inputStream, 1024 * 128), "UTF-8"); this.output = new OutputStreamWriter(new BufferedOutputStream(outputStream), "UTF-8"); this.outputStream = null; @@@ -669,6 -678,8 +678,8 @@@ for (int i = 0; i < height; i++) { display.add(new DisplayLine(currentState.attr)); } + assert (currentState.cursorY < height); + assert (currentState.cursorX < width); // Spin up the input reader readerThread = new Thread(this); @@@ -760,11 -771,28 +771,28 @@@ int ch = Character.codePointAt(readBufferUTF8, i); i += Character.charCount(ch); - consume(ch); + + // Special case for VT10x: 7-bit characters + // only. + if ((type == DeviceType.VT100) + || (type == DeviceType.VT102) + ) { + consume(ch & 0x7F); + } else { + consume(ch); + } } } else { for (int i = 0; i < rc; i++) { - consume(readBuffer[i]); + // Special case for VT10x: 7-bit characters + // only. + if ((type == DeviceType.VT100) + || (type == DeviceType.VT102) + ) { + consume(readBuffer[i] & 0x7F); + } else { + consume(readBuffer[i]); + } } } } @@@ -831,6 -859,34 +859,34 @@@ // ECMA48 ----------------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Wait for a period of time to get output from the launched process. + * + * @param millis millis to wait for, or 0 to wait forever + * @return true if the launched process has emitted something + */ + public boolean waitForOutput(final int millis) { + if (millis < 0) { + throw new IllegalArgumentException("timeout must be >= 0"); + } + int waitedMillis = millis; + final int pollTimeout = 5; + while (true) { + if (readCount != 0) { + return true; + } + if ((millis > 0) && (waitedMillis < 0)){ + return false; + } + try { + Thread.sleep(pollTimeout); + } catch (InterruptedException e) { + // SQUASH + } + waitedMillis -= pollTimeout; + } + } + /** * Process keyboard and mouse events from the user. * @@@ -874,14 -930,14 +930,14 @@@ case VT220: case XTERM: - // "I am a VT220" - 7 bit version + // "I am a VT220" - 7 bit version, with sixel and Jexer image + // support. if (!s8c1t) { - return "\033[?62;1;6;9;4;22c"; - // return "\033[?62;1;6;9;4;22;444c"; + return "\033[?62;1;6;9;4;22;444c"; } - // "I am a VT220" - 8 bit version - return "\u009b?62;1;6;9;4;22c"; - // return "\u009b?62;1;6;9;4;22;444c"; + // "I am a VT220" - 8 bit version, with sixel and Jexer image + // support. + return "\u009b?62;1;6;9;4;22;444c"; default: throw new IllegalArgumentException("Invalid device type: " + type); } @@@ -1002,11 -1058,6 +1058,6 @@@ // the input streams. if (stopReaderThread == false) { stopReaderThread = true; - try { - readerThread.join(1000); - } catch (InterruptedException e) { - // SQUASH - } } // Now close the output stream. @@@ -1186,8 -1237,8 +1237,8 @@@ int delta = height - this.height; this.height = height; scrollRegionBottom += delta; - if (scrollRegionBottom < 0) { - scrollRegionBottom = height; + if ((scrollRegionBottom < 0) || (scrollRegionTop > height - 1)) { + scrollRegionBottom = height - 1; } if (scrollRegionTop >= scrollRegionBottom) { scrollRegionTop = 0; @@@ -1204,10 -1255,29 +1255,29 @@@ display.add(line); } while (display.size() > height) { - scrollback.add(display.remove(0)); + appendScrollbackLine(display.remove(0)); } } + /** + * Get the maximum number of lines in the scrollback buffer. + * + * @return the maximum number of lines in the scrollback buffer + */ + public int getScrollbackMax() { + return scrollbackMax; + } + + /** + * Set the maximum number of lines for the scrollback buffer. + * + * @param scrollbackMax the maximum number of lines for the scrollback + * buffer + */ + public final void setScrollbackMax(final int scrollbackMax) { + this.scrollbackMax = scrollbackMax; + } + /** * Get visible cursor flag. * @@@ -1242,7 -1312,7 +1312,7 @@@ */ private void toGround() { csiParams.clear(); - collectBuffer = new StringBuilder(8); + collectBuffer.setLength(0); scanState = ScanState.GROUND; } @@@ -1265,7 -1335,7 +1335,7 @@@ colors88.add(0); } - // Set default system colors. + // Set default system colors. These match DOS colors. colors88.set(0, 0x00000000); colors88.set(1, 0x00a80000); colors88.set(2, 0x0000a800); @@@ -1283,6 -1353,249 +1353,249 @@@ colors88.set(13, 0x00fc54fc); colors88.set(14, 0x0054fcfc); colors88.set(15, 0x00fcfcfc); + + // These match xterm's default colors from 256colres.h. + colors88.set(16, 0x000000); + colors88.set(17, 0x00005f); + colors88.set(18, 0x000087); + colors88.set(19, 0x0000af); + colors88.set(20, 0x0000d7); + colors88.set(21, 0x0000ff); + colors88.set(22, 0x005f00); + colors88.set(23, 0x005f5f); + colors88.set(24, 0x005f87); + colors88.set(25, 0x005faf); + colors88.set(26, 0x005fd7); + colors88.set(27, 0x005fff); + colors88.set(28, 0x008700); + colors88.set(29, 0x00875f); + colors88.set(30, 0x008787); + colors88.set(31, 0x0087af); + colors88.set(32, 0x0087d7); + colors88.set(33, 0x0087ff); + colors88.set(34, 0x00af00); + colors88.set(35, 0x00af5f); + colors88.set(36, 0x00af87); + colors88.set(37, 0x00afaf); + colors88.set(38, 0x00afd7); + colors88.set(39, 0x00afff); + colors88.set(40, 0x00d700); + colors88.set(41, 0x00d75f); + colors88.set(42, 0x00d787); + colors88.set(43, 0x00d7af); + colors88.set(44, 0x00d7d7); + colors88.set(45, 0x00d7ff); + colors88.set(46, 0x00ff00); + colors88.set(47, 0x00ff5f); + colors88.set(48, 0x00ff87); + colors88.set(49, 0x00ffaf); + colors88.set(50, 0x00ffd7); + colors88.set(51, 0x00ffff); + colors88.set(52, 0x5f0000); + colors88.set(53, 0x5f005f); + colors88.set(54, 0x5f0087); + colors88.set(55, 0x5f00af); + colors88.set(56, 0x5f00d7); + colors88.set(57, 0x5f00ff); + colors88.set(58, 0x5f5f00); + colors88.set(59, 0x5f5f5f); + colors88.set(60, 0x5f5f87); + colors88.set(61, 0x5f5faf); + colors88.set(62, 0x5f5fd7); + colors88.set(63, 0x5f5fff); + colors88.set(64, 0x5f8700); + colors88.set(65, 0x5f875f); + colors88.set(66, 0x5f8787); + colors88.set(67, 0x5f87af); + colors88.set(68, 0x5f87d7); + colors88.set(69, 0x5f87ff); + colors88.set(70, 0x5faf00); + colors88.set(71, 0x5faf5f); + colors88.set(72, 0x5faf87); + colors88.set(73, 0x5fafaf); + colors88.set(74, 0x5fafd7); + colors88.set(75, 0x5fafff); + colors88.set(76, 0x5fd700); + colors88.set(77, 0x5fd75f); + colors88.set(78, 0x5fd787); + colors88.set(79, 0x5fd7af); + colors88.set(80, 0x5fd7d7); + colors88.set(81, 0x5fd7ff); + colors88.set(82, 0x5fff00); + colors88.set(83, 0x5fff5f); + colors88.set(84, 0x5fff87); + colors88.set(85, 0x5fffaf); + colors88.set(86, 0x5fffd7); + colors88.set(87, 0x5fffff); + colors88.set(88, 0x870000); + colors88.set(89, 0x87005f); + colors88.set(90, 0x870087); + colors88.set(91, 0x8700af); + colors88.set(92, 0x8700d7); + colors88.set(93, 0x8700ff); + colors88.set(94, 0x875f00); + colors88.set(95, 0x875f5f); + colors88.set(96, 0x875f87); + colors88.set(97, 0x875faf); + colors88.set(98, 0x875fd7); + colors88.set(99, 0x875fff); + colors88.set(100, 0x878700); + colors88.set(101, 0x87875f); + colors88.set(102, 0x878787); + colors88.set(103, 0x8787af); + colors88.set(104, 0x8787d7); + colors88.set(105, 0x8787ff); + colors88.set(106, 0x87af00); + colors88.set(107, 0x87af5f); + colors88.set(108, 0x87af87); + colors88.set(109, 0x87afaf); + colors88.set(110, 0x87afd7); + colors88.set(111, 0x87afff); + colors88.set(112, 0x87d700); + colors88.set(113, 0x87d75f); + colors88.set(114, 0x87d787); + colors88.set(115, 0x87d7af); + colors88.set(116, 0x87d7d7); + colors88.set(117, 0x87d7ff); + colors88.set(118, 0x87ff00); + colors88.set(119, 0x87ff5f); + colors88.set(120, 0x87ff87); + colors88.set(121, 0x87ffaf); + colors88.set(122, 0x87ffd7); + colors88.set(123, 0x87ffff); + colors88.set(124, 0xaf0000); + colors88.set(125, 0xaf005f); + colors88.set(126, 0xaf0087); + colors88.set(127, 0xaf00af); + colors88.set(128, 0xaf00d7); + colors88.set(129, 0xaf00ff); + colors88.set(130, 0xaf5f00); + colors88.set(131, 0xaf5f5f); + colors88.set(132, 0xaf5f87); + colors88.set(133, 0xaf5faf); + colors88.set(134, 0xaf5fd7); + colors88.set(135, 0xaf5fff); + colors88.set(136, 0xaf8700); + colors88.set(137, 0xaf875f); + colors88.set(138, 0xaf8787); + colors88.set(139, 0xaf87af); + colors88.set(140, 0xaf87d7); + colors88.set(141, 0xaf87ff); + colors88.set(142, 0xafaf00); + colors88.set(143, 0xafaf5f); + colors88.set(144, 0xafaf87); + colors88.set(145, 0xafafaf); + colors88.set(146, 0xafafd7); + colors88.set(147, 0xafafff); + colors88.set(148, 0xafd700); + colors88.set(149, 0xafd75f); + colors88.set(150, 0xafd787); + colors88.set(151, 0xafd7af); + colors88.set(152, 0xafd7d7); + colors88.set(153, 0xafd7ff); + colors88.set(154, 0xafff00); + colors88.set(155, 0xafff5f); + colors88.set(156, 0xafff87); + colors88.set(157, 0xafffaf); + colors88.set(158, 0xafffd7); + colors88.set(159, 0xafffff); + colors88.set(160, 0xd70000); + colors88.set(161, 0xd7005f); + colors88.set(162, 0xd70087); + colors88.set(163, 0xd700af); + colors88.set(164, 0xd700d7); + colors88.set(165, 0xd700ff); + colors88.set(166, 0xd75f00); + colors88.set(167, 0xd75f5f); + colors88.set(168, 0xd75f87); + colors88.set(169, 0xd75faf); + colors88.set(170, 0xd75fd7); + colors88.set(171, 0xd75fff); + colors88.set(172, 0xd78700); + colors88.set(173, 0xd7875f); + colors88.set(174, 0xd78787); + colors88.set(175, 0xd787af); + colors88.set(176, 0xd787d7); + colors88.set(177, 0xd787ff); + colors88.set(178, 0xd7af00); + colors88.set(179, 0xd7af5f); + colors88.set(180, 0xd7af87); + colors88.set(181, 0xd7afaf); + colors88.set(182, 0xd7afd7); + colors88.set(183, 0xd7afff); + colors88.set(184, 0xd7d700); + colors88.set(185, 0xd7d75f); + colors88.set(186, 0xd7d787); + colors88.set(187, 0xd7d7af); + colors88.set(188, 0xd7d7d7); + colors88.set(189, 0xd7d7ff); + colors88.set(190, 0xd7ff00); + colors88.set(191, 0xd7ff5f); + colors88.set(192, 0xd7ff87); + colors88.set(193, 0xd7ffaf); + colors88.set(194, 0xd7ffd7); + colors88.set(195, 0xd7ffff); + colors88.set(196, 0xff0000); + colors88.set(197, 0xff005f); + colors88.set(198, 0xff0087); + colors88.set(199, 0xff00af); + colors88.set(200, 0xff00d7); + colors88.set(201, 0xff00ff); + colors88.set(202, 0xff5f00); + colors88.set(203, 0xff5f5f); + colors88.set(204, 0xff5f87); + colors88.set(205, 0xff5faf); + colors88.set(206, 0xff5fd7); + colors88.set(207, 0xff5fff); + colors88.set(208, 0xff8700); + colors88.set(209, 0xff875f); + colors88.set(210, 0xff8787); + colors88.set(211, 0xff87af); + colors88.set(212, 0xff87d7); + colors88.set(213, 0xff87ff); + colors88.set(214, 0xffaf00); + colors88.set(215, 0xffaf5f); + colors88.set(216, 0xffaf87); + colors88.set(217, 0xffafaf); + colors88.set(218, 0xffafd7); + colors88.set(219, 0xffafff); + colors88.set(220, 0xffd700); + colors88.set(221, 0xffd75f); + colors88.set(222, 0xffd787); + colors88.set(223, 0xffd7af); + colors88.set(224, 0xffd7d7); + colors88.set(225, 0xffd7ff); + colors88.set(226, 0xffff00); + colors88.set(227, 0xffff5f); + colors88.set(228, 0xffff87); + colors88.set(229, 0xffffaf); + colors88.set(230, 0xffffd7); + colors88.set(231, 0xffffff); + colors88.set(232, 0x080808); + colors88.set(233, 0x121212); + colors88.set(234, 0x1c1c1c); + colors88.set(235, 0x262626); + colors88.set(236, 0x303030); + colors88.set(237, 0x3a3a3a); + colors88.set(238, 0x444444); + colors88.set(239, 0x4e4e4e); + colors88.set(240, 0x585858); + colors88.set(241, 0x626262); + colors88.set(242, 0x6c6c6c); + colors88.set(243, 0x767676); + colors88.set(244, 0x808080); + colors88.set(245, 0x8a8a8a); + colors88.set(246, 0x949494); + colors88.set(247, 0x9e9e9e); + colors88.set(248, 0xa8a8a8); + colors88.set(249, 0xb2b2b2); + colors88.set(250, 0xbcbcbc); + colors88.set(251, 0xc6c6c6); + colors88.set(252, 0xd0d0d0); + colors88.set(253, 0xdadada); + colors88.set(254, 0xe4e4e4); + colors88.set(255, 0xeeeeee); + } /** @@@ -1357,8 -1670,13 +1670,13 @@@ currentState = new SaveableState(); savedState = new SaveableState(); scanState = ScanState.GROUND; - width = 80; - height = 24; + if (displayListener != null) { + width = displayListener.getDisplayWidth(); + height = displayListener.getDisplayHeight(); + } else { + width = 80; + height = 24; + } scrollRegionTop = 0; scrollRegionBottom = height - 1; rightMargin = width - 1; @@@ -1366,11 -1684,6 +1684,6 @@@ arrowKeyMode = ArrowKeyMode.ANSI; keypadMode = KeypadMode.Numeric; wrapLineFlag = false; - if (displayListener != null) { - width = displayListener.getDisplayWidth(); - height = displayListener.getDisplayHeight(); - rightMargin = width - 1; - } // Flags shiftOut = false; @@@ -1401,14 -1714,25 +1714,25 @@@ toGround(); } + /** + * Append a to the scrollback buffer, clearing image data for lines more + * than three screenfuls in. + */ + private void appendScrollbackLine(DisplayLine line) { + scrollback.add(line); + if (scrollback.size() > height * 3) { + scrollback.get(scrollback.size() - (height * 3)).clearImages(); + } + } + /** * Append a new line to the bottom of the display, adding lines off the * top to the scrollback buffer. */ private void newDisplayLine() { // Scroll the top line off into the scrollback buffer - scrollback.add(display.get(0)); - if (scrollback.size() > maxScrollback) { + appendScrollbackLine(display.get(0)); + while (scrollback.size() > scrollbackMax) { scrollback.remove(0); scrollback.trimToSize(); } @@@ -1453,7 -1777,6 +1777,6 @@@ * Handle a linefeed. */ private void linefeed() { - if (currentState.cursorY < scrollRegionBottom) { // Increment screen y currentState.cursorY++; @@@ -1664,35 -1987,45 +1987,45 @@@ if (mouseEncoding == MouseEncoding.SGR) { sb.append((char) 0x1B); sb.append("[<"); + int buttons = 0; if (mouse.isMouse1()) { if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) { - sb.append("32;"); + buttons = 32; } else { - sb.append("0;"); + buttons = 0; } } else if (mouse.isMouse2()) { if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) { - sb.append("33;"); + buttons = 33; } else { - sb.append("1;"); + buttons = 1; } } else if (mouse.isMouse3()) { if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) { - sb.append("34;"); + buttons = 34; } else { - sb.append("2;"); + buttons = 2; } } else if (mouse.isMouseWheelUp()) { - sb.append("64;"); + buttons = 64; } else if (mouse.isMouseWheelDown()) { - sb.append("65;"); + buttons = 65; } else { // This is motion with no buttons down. - sb.append("35;"); + buttons = 35; + } + if (mouse.isAlt()) { + buttons |= 0x08; + } + if (mouse.isCtrl()) { + buttons |= 0x10; + } + if (mouse.isShift()) { + buttons |= 0x04; } - sb.append(String.format("%d;%d", mouse.getX() + 1, + sb.append(String.format("%d;%d;%d", buttons, mouse.getX() + 1, mouse.getY() + 1)); if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) { @@@ -1706,35 -2039,46 +2039,46 @@@ sb.append((char) 0x1B); sb.append('['); sb.append('M'); + int buttons = 0; if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) { - sb.append((char) (0x03 + 32)); + buttons = 0x03 + 32; } else if (mouse.isMouse1()) { if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) { - sb.append((char) (0x00 + 32 + 32)); + buttons = 0x00 + 32 + 32; } else { - sb.append((char) (0x00 + 32)); + buttons = 0x00 + 32; } } else if (mouse.isMouse2()) { if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) { - sb.append((char) (0x01 + 32 + 32)); + buttons = 0x01 + 32 + 32; } else { - sb.append((char) (0x01 + 32)); + buttons = 0x01 + 32; } } else if (mouse.isMouse3()) { if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) { - sb.append((char) (0x02 + 32 + 32)); + buttons = 0x02 + 32 + 32; } else { - sb.append((char) (0x02 + 32)); + buttons = 0x02 + 32; } } else if (mouse.isMouseWheelUp()) { - sb.append((char) (0x04 + 64)); + buttons = 0x04 + 64; } else if (mouse.isMouseWheelDown()) { - sb.append((char) (0x05 + 64)); + buttons = 0x05 + 64; } else { // This is motion with no buttons down. - sb.append((char) (0x03 + 32)); + buttons = 0x03 + 32; + } + if (mouse.isAlt()) { + buttons |= 0x08; + } + if (mouse.isCtrl()) { + buttons |= 0x10; + } + if (mouse.isShift()) { + buttons |= 0x04; } + sb.append((char) (buttons & 0xFF)); sb.append((char) (mouse.getX() + 33)); sb.append((char) (mouse.getY() + 33)); } @@@ -3156,10 -3500,10 +3500,10 @@@ if (decPrivateModeFlag == true) { if (value == true) { // Enable sixel scrolling (default). - // TODO + // Not supported } else { // Disable sixel scrolling. - // TODO + // Not supported } } } @@@ -3939,14 -4283,14 +4283,14 @@@ * RGB color mode. */ rgbColor = true; - break; + continue; case 5: /* * Indexed color mode. */ idx88Color = true; - break; + continue; default: /* @@@ -3994,7 -4338,7 +4338,7 @@@ case 8: // Invisible - // TODO + // Not supported break; case 90: @@@ -4379,6 -4723,9 +4723,9 @@@ // DECSTBM int top = getCsiParam(0, 1, 1, height) - 1; int bottom = getCsiParam(1, height, 1, height) - 1; + if (bottom > height - 1) { + bottom = height - 1; + } if (top > bottom) { top = bottom; @@@ -4732,13 -5079,22 +5079,22 @@@ private void oscPut(final char xtermChar) { // System.err.println("oscPut: " + xtermChar); + boolean oscEnd = false; + + if (xtermChar == 0x07) { + oscEnd = true; + } + if ((xtermChar == '\\') + && (collectBuffer.charAt(collectBuffer.length() - 1) == '\033') + ) { + oscEnd = true; + } + // Collect first collectBuffer.append(xtermChar); // Xterm cases... - if ((xtermChar == 0x07) - || (collectBuffer.toString().endsWith("\033\\")) - ) { + if (oscEnd) { String args = null; if (xtermChar == 0x07) { args = collectBuffer.substring(0, collectBuffer.length() - 1); @@@ -4792,11 -5148,18 +5148,18 @@@ } } - if (p[0].equals("444") && (p.length == 5)) { - // Jexer image - parseJexerImage(p[1], p[2], p[3], p[4]); + if (p[0].equals("444")) { + if (p[1].equals("0") && (p.length == 6)) { + // Jexer image - RGB + parseJexerImageRGB(p[2], p[3], p[4], p[5]); + } else if (p[1].equals("1") && (p.length == 4)) { + // Jexer image - PNG + parseJexerImageFile(1, p[2], p[3]); + } else if (p[1].equals("2") && (p.length == 4)) { + // Jexer image - JPG + parseJexerImageFile(2, p[2], p[3]); + } } - } // Go to SCAN_GROUND state @@@ -4814,11 -5177,19 +5177,19 @@@ private void pmPut(final char pmChar) { // System.err.println("pmPut: " + pmChar); + boolean pmEnd = false; + + if ((pmChar == '\\') + && (collectBuffer.charAt(collectBuffer.length() - 1) == '\033') + ) { + pmEnd = true; + } + // Collect first collectBuffer.append(pmChar); // Xterm cases... - if (collectBuffer.toString().endsWith("\033\\")) { + if (pmEnd) { String arg = null; arg = collectBuffer.substring(0, collectBuffer.length() - 2); @@@ -4905,16 -5276,12 +5276,12 @@@ * * @param ch character from the remote side */ - private void consume(int ch) { + private void consume(final int ch) { + readCount++; // DEBUG // System.err.printf("%c STATE = %s\n", ch, scanState); - // Special case for VT10x: 7-bit characters only - if ((type == DeviceType.VT100) || (type == DeviceType.VT102)) { - ch = (ch & 0x7F); - } - // Special "anywhere" states // 18, 1A --> execute, then switch to SCAN_GROUND @@@ -6687,7 -7054,7 +7054,7 @@@ // 0x71 goes to DCS_SIXEL if (ch == 0x71) { - sixelParseBuffer = new StringBuilder(); + sixelParseBuffer.setLength(0); scanState = ScanState.DCS_SIXEL; } else if ((ch >= 0x40) && (ch <= 0x7E)) { // 0x40-7E goes to DCS_PASSTHROUGH @@@ -6772,7 -7139,7 +7139,7 @@@ // 0x71 goes to DCS_SIXEL if (ch == 0x71) { - sixelParseBuffer = new StringBuilder(); + sixelParseBuffer.setLength(0); scanState = ScanState.DCS_SIXEL; } else if ((ch >= 0x40) && (ch <= 0x7E)) { // 0x40-7E goes to DCS_PASSTHROUGH @@@ -7036,87 -7403,19 +7403,19 @@@ // Sixel data was malformed in some way, bail out. return; } - - /* - * Procedure: - * - * Break up the image into text cell sized pieces as a new array of - * Cells. - * - * Note original column position x0. - * - * For each cell: - * - * 1. Advance (printCharacter(' ')) for horizontal increment, or - * index (linefeed() + cursorPosition(y, x0)) for vertical - * increment. - * - * 2. Set (x, y) cell image data. - * - * 3. For the right and bottom edges: - * - * a. Render the text to pixels using Terminus font. - * - * b. Blit the image on top of the text, using alpha channel. - */ - int cellColumns = image.getWidth() / textWidth; - if (cellColumns * textWidth < image.getWidth()) { - cellColumns++; - } - int cellRows = image.getHeight() / textHeight; - if (cellRows * textHeight < image.getHeight()) { - cellRows++; - } - - // Break the image up into an array of cells. - Cell [][] cells = new Cell[cellColumns][cellRows]; - - for (int x = 0; x < cellColumns; x++) { - for (int y = 0; y < cellRows; y++) { - - int width = textWidth; - if ((x + 1) * textWidth > image.getWidth()) { - width = image.getWidth() - (x * textWidth); - } - int height = textHeight; - if ((y + 1) * textHeight > image.getHeight()) { - height = image.getHeight() - (y * textHeight); - } - - Cell cell = new Cell(); - cell.setImage(image.getSubimage(x * textWidth, - y * textHeight, width, height)); - - cells[x][y] = cell; - } - } - - int x0 = currentState.cursorX; - for (int y = 0; y < cellRows; y++) { - for (int x = 0; x < cellColumns; x++) { - assert (currentState.cursorX <= rightMargin); - - // TODO: Render text of current cell first, then image over - // it (accounting for blank pixels). For now, just copy the - // cell. - DisplayLine line = display.get(currentState.cursorY); - line.replace(currentState.cursorX, cells[x][y]); - - // If at the end of the visible screen, stop. - if (currentState.cursorX == rightMargin) { - break; - } - // Room for more image on the visible screen. - currentState.cursorX++; - } - linefeed(); - cursorPosition(currentState.cursorY, x0); + if ((image.getWidth() < 1) + || (image.getWidth() > 10000) + || (image.getHeight() < 1) + || (image.getHeight() > 10000) + ) { + return; } + imageToCells(image, true); } /** - * Parse a "Jexer" image string into a bitmap image, and overlay that + * Parse a "Jexer" RGB image string into a bitmap image, and overlay that * image onto the text cells. * * @param pw width token @@@ -7124,7 -7423,7 +7423,7 @@@ * @param ps scroll token * @param data pixel data */ - private void parseJexerImage(final String pw, final String ph, + private void parseJexerImageRGB(final String pw, final String ph, final String ps, final String data) { int imageWidth = 0; @@@ -7152,8 -7451,7 +7451,7 @@@ return; } - java.util.Base64.Decoder base64 = java.util.Base64.getDecoder(); - byte [] bytes = base64.decode(data); + byte [] bytes = StringUtils.fromBase64(data.getBytes()); if (bytes.length != (imageWidth * imageHeight * 3)) { return; } @@@ -7180,6 -7478,93 +7478,93 @@@ } } + imageToCells(image, scroll); + } + + /** + * Parse a "Jexer" PNG or JPG image string into a bitmap image, and + * overlay that image onto the text cells. + * + * @param type 1 for PNG, 2 for JPG + * @param ps scroll token + * @param data pixel data + */ + private void parseJexerImageFile(final int type, final String ps, + final String data) { + + int imageWidth = 0; + int imageHeight = 0; + boolean scroll = false; + BufferedImage image = null; + try { + byte [] bytes = StringUtils.fromBase64(data.getBytes()); + + switch (type) { + case 1: + if ((bytes[0] != (byte) 0x89) + || (bytes[1] != 'P') + || (bytes[2] != 'N') + || (bytes[3] != 'G') + || (bytes[4] != (byte) 0x0D) + || (bytes[5] != (byte) 0x0A) + || (bytes[6] != (byte) 0x1A) + || (bytes[7] != (byte) 0x0A) + ) { + // File does not have PNG header, bail out. + return; + } + break; + + case 2: + if ((bytes[0] != (byte) 0XFF) + || (bytes[1] != (byte) 0xD8) + || (bytes[2] != (byte) 0xFF) + ) { + // File does not have JPG header, bail out. + return; + } + break; + + default: + // Unsupported type, bail out. + return; + } + + image = ImageIO.read(new ByteArrayInputStream(bytes)); + } catch (IOException e) { + // SQUASH + return; + } + assert (image != null); + imageWidth = image.getWidth(); + imageHeight = image.getHeight(); + if ((imageWidth < 1) + || (imageWidth > 10000) + || (imageHeight < 1) + || (imageHeight > 10000) + ) { + return; + } + if (ps.equals("1")) { + scroll = true; + } else if (ps.equals("0")) { + scroll = false; + } else { + return; + } + + imageToCells(image, scroll); + } + + /** + * Break up an image into the cells at the current cursor. + * + * @param image the image to display + * @param scroll if true, scroll the image and move the cursor + */ + private void imageToCells(final BufferedImage image, final boolean scroll) { + assert (image != null); + /* * Procedure: * @@@ -7227,19 -7612,39 +7612,39 @@@ } Cell cell = new Cell(); - cell.setImage(image.getSubimage(x * textWidth, - y * textHeight, width, height)); + if ((width != textWidth) || (height != textHeight)) { + BufferedImage newImage; + newImage = new BufferedImage(textWidth, textHeight, + BufferedImage.TYPE_INT_ARGB); + + Graphics gr = newImage.getGraphics(); + gr.drawImage(image.getSubimage(x * textWidth, + y * textHeight, width, height), + 0, 0, null, null); + gr.dispose(); + cell.setImage(newImage); + } else { + cell.setImage(image.getSubimage(x * textWidth, + y * textHeight, width, height)); + } cells[x][y] = cell; } } int x0 = currentState.cursorX; + int y0 = currentState.cursorY; for (int y = 0; y < cellRows; y++) { for (int x = 0; x < cellColumns; x++) { assert (currentState.cursorX <= rightMargin); + + // A real sixel terminal would render the text of the current + // cell first, then image over it (accounting for blank + // pixels). We do not support that. A cell is either text, + // or image, but not a mix of image-over-text. DisplayLine line = display.get(currentState.cursorY); line.replace(currentState.cursorX, cells[x][y]); + // If at the end of the visible screen, stop. if (currentState.cursorX == rightMargin) { break; @@@ -7247,15 -7652,24 +7652,24 @@@ // Room for more image on the visible screen. currentState.cursorX++; } - if ((scroll == true) - || ((scroll == false) - && (currentState.cursorY < scrollRegionBottom)) - ) { + if (currentState.cursorY < scrollRegionBottom - 1) { + // Not at the bottom, down a line. linefeed(); + } else if (scroll == true) { + // At the bottom, scroll as needed. + linefeed(); + } else { + // At the bottom, no more scrolling, done. + break; } + cursorPosition(currentState.cursorY, x0); } + if (scroll == false) { + cursorPosition(y0, x0); + } + } } diff --combined tterminal/Sixel.java index a4c00fc,b91e77a..b91e77a --- a/tterminal/Sixel.java +++ b/tterminal/Sixel.java @@@ -31,7 -31,6 +31,6 @@@ package jexer.tterminal import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; - import java.util.ArrayList; import java.util.HashMap; /** @@@ -574,10 -573,10 +573,10 @@@ public class Sixel case REPEAT: if ((ch >= '0') && (ch <= '9')) { if (repeatCount == -1) { - repeatCount = (int) (ch - '0'); + repeatCount = (ch - '0'); } else { repeatCount *= 10; - repeatCount += (int) (ch - '0'); + repeatCount += (ch - '0'); } } return; diff --combined ttree/TTreeViewWidget.java index 080a200,13beac3..13beac3 --- a/ttree/TTreeViewWidget.java +++ b/ttree/TTreeViewWidget.java @@@ -268,11 -268,55 +268,55 @@@ public class TTreeViewWidget extends TS // TScrollableWidget ------------------------------------------------------ // ------------------------------------------------------------------------ + /** + * Override TWidget's width: we need to set child widget widths. + * + * @param width new widget width + */ + @Override + public void setWidth(final int width) { + super.setWidth(width); + if (hScroller != null) { + hScroller.setWidth(getWidth() - 1); + } + if (vScroller != null) { + vScroller.setX(getWidth() - 1); + } + if (treeView != null) { + treeView.setWidth(getWidth() - 1); + } + reflowData(); + } + + /** + * Override TWidget's height: we need to set child widget heights. + * + * @param height new widget height + */ + @Override + public void setHeight(final int height) { + super.setHeight(height); + if (hScroller != null) { + hScroller.setY(getHeight() - 1); + } + if (vScroller != null) { + vScroller.setHeight(getHeight() - 1); + } + if (treeView != null) { + treeView.setHeight(getHeight() - 1); + } + reflowData(); + } + /** * Resize text and scrollbars for a new width/height. */ @Override public void reflowData() { + if (treeView == null) { + return; + } + int selectedRow = 0; boolean foundSelectedRow = false;