From: Niki Roo Date: Thu, 2 Jan 2020 17:20:14 +0000 (+0100) Subject: Merge branch 'upstream-sep2019-tcombo' into subtree X-Git-Url: http://git.nikiroo.be/?p=nikiroo-utils.git;a=commitdiff_plain;h=77d3a60869e7a780c6ae069e51530e1eacece5e2;hp=35b1874b53e81e93aea42f146decc138c71ca0d1 Merge branch 'upstream-sep2019-tcombo' into subtree --- diff --git a/EditMenuUser.java b/EditMenuUser.java new file mode 100644 index 0000000..52dc33e --- /dev/null +++ b/EditMenuUser.java @@ -0,0 +1,66 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer; + +/** + * EditMenuUser is used by TApplication to enable/disable edit menu items. A + * widget that supports these functions should define an onCommand method + * that operates on cmCut, cmCopy, cmPaste, and cmClear. + */ +public interface EditMenuUser { + + /** + * Check if the cut menu item should be enabled. + * + * @return true if the cut menu item should be enabled + */ + public boolean isEditMenuCut(); + + /** + * Check if the copy menu item should be enabled. + * + * @return true if the copy menu item should be enabled + */ + public boolean isEditMenuCopy(); + + /** + * Check if the paste menu item should be enabled. + * + * @return true if the paste menu item should be enabled + */ + public boolean isEditMenuPaste(); + + /** + * Check if the clear menu item should be enabled. + * + * @return true if the clear menu item should be enabled + */ + public boolean isEditMenuClear(); + +} diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 7b02f56..0000000 --- a/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013-2019 Kevin Lamonte - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index 4e6127a..0000000 --- a/README.md +++ /dev/null @@ -1,201 +0,0 @@ -Jexer - Java Text User Interface library -======================================== - -This library implements a text-based windowing system loosely -reminiscent of Borland's [Turbo -Vision](http://en.wikipedia.org/wiki/Turbo_Vision) system. It looks -like this: - -![Terminal, Image, Table](/screenshots/new_demo1.png?raw=true "Terminal, Image, Table") - -Jexer works on both Xterm-like terminals and Swing, and supports -images in both Xterm and Swing. On Swing, images are true color: - -![Swing Snake Image](/screenshots/snake_swing.png?raw=true "Swing Snake Image") - -On Xterm, images are dithered to a common palette: - -![Xterm Snake Image](/screenshots/snake_xterm.png?raw=true "Xterm Snake Image") - - - -License -------- - -Jexer is available to all under the MIT License. See the file LICENSE -for the full license text. - - - -Obtaining Jexer ---------------- - -Jexer is available on Maven Central: - -```xml - - com.gitlab.klamonte - jexer - 0.3.2 - -``` - -Binary releases are available on SourceForge: -https://sourceforge.net/projects/jexer/files/jexer/ - -The Jexer source code is hosted at: https://gitlab.com/klamonte/jexer - - - -Documentation -------------- - -* [Java API Docs](https://jexer.sourceforge.io/apidocs/api/index.html) - -* [Wiki](https://gitlab.com/klamonte/jexer/wikis/home) - -* [Jexer web page](https://jexer.sourceforge.io/) - - - -Programming Examples --------------------- - -The examples/ folder currently contains: - - * A [prototype tiling window - manager](/examples/JexerTilingWindowManager.java) in less than 250 - lines of code. - - * A much slicker [prototype tiling window - manager](/examples/JexerTilingWindowManager2.java) in less than 200 - lines of code. - - * A [prototype image thumbnail - viewer](/examples/JexerImageViewer.java) in less than 350 lines of - code. - -jexer.demos contains official demos showing all of the existing UI -controls. The demos can be run as follows: - - * 'java -jar jexer.jar' . This will use System.in/out with - Xterm-like sequences on non-Windows non-Mac platforms. On Windows - and Mac it will use a Swing JFrame. - - * 'java -Djexer.Swing=true -jar jexer.jar' . This will always use - Swing on any platform. - - * 'java -cp jexer.jar jexer.demos.Demo2 PORT' (where PORT is a - number to run the TCP daemon on). This will use the Xterm backend - on a telnet server that will update with screen size changes. - - * 'java -cp jexer.jar jexer.demos.Demo3' . This will use - System.in/out with Xterm-like sequences. One can see in the code - how to pass a different InputReader and OutputReader to - TApplication, permitting a different encoding than UTF-8. - - * 'java -cp jexer.jar jexer.demos.Demo4' . This demonstrates hidden - windows and a custom TDesktop. - - * 'java -cp jexer.jar jexer.demos.Demo5' . This demonstrates two - demo applications using different fonts in the same Swing frame. - - * 'java -cp jexer.jar jexer.demos.Demo6' . This demonstrates two - applications performing I/O across three screens: an Xterm screen - and Swing screen, monitored from a third Swing screen. - - * 'java -cp jexer.jar jexer.demos.Demo7' . This demonstrates the - BoxLayoutManager, achieving a similar result as the - javax.swing.BoxLayout apidocs example. - - - -More Screenshots ----------------- - -Jexer can be run inside its own terminal window, with support for all -of its features including images and mouse, and more terminals: - -![Yo Dawg...](/screenshots/jexer_sixel_in_sixel.png?raw=true "Yo Dawg, I heard you like text windowing systems, so I ran a text windowing system inside your text windowing system so you can have a terminal in your terminal.") - -Sixel output uses a single palette which works OK for a variety of -real-world images: - -![Sixel Pictures Of Cliffs Of Moher And Buoy](/screenshots/sixel_images.png?raw=true "Sixel Pictures Of Cliffs Of Moher And Buoy") - -The color wheel with that palette is shown below: - -![Sixel Color Wheel](/screenshots/sixel_color_wheel.png?raw=true "Sixel Color Wheel") - - - -Terminal Support ----------------- - -The table below lists terminals tested against Jexer's Xterm backend: - -| Terminal | Environment | Mouse Click | Mouse Cursor | Images | -| -------------- | ------------------ | ----------- | ------------ | ------ | -| xterm | X11 | yes | yes | yes | -| jexer | CLI, X11, Windows | yes | yes | yes | -| mlterm | X11 | yes | yes | yes | -| RLogin | Windows | yes | yes | yes | -| alacritty(3) | X11 | yes | yes | no | -| gnome-terminal | X11 | yes | yes | no | -| iTerm2 | Mac | yes | yes | no(5) | -| kitty(3) | X11 | yes | yes | no | -| lcxterm(3) | CLI, Linux console | yes | yes | no | -| mintty | Windows | yes | yes | no(5) | -| rxvt-unicode | X11 | yes | yes | no(2) | -| xfce4-terminal | X11 | yes | yes | no | -| aminal(3) | X11 | yes | no | no | -| konsole | X11 | yes | no | no | -| yakuake | X11 | yes | no | no | -| Windows Terminal(6) | Windows | no | no | no(2) | -| screen | CLI | yes(1) | yes(1) | no(2) | -| tmux | CLI | yes(1) | yes(1) | no | -| putty | X11, Windows | yes | no | no(2) | -| Linux | Linux console | no | no | no(2) | -| qodem(3) | CLI, Linux console | yes | yes(4) | no | -| qodem-x11(3) | X11 | yes | no | no | -| yaft | Linux console (FB) | no | no | yes | - -1 - Requires mouse support from host terminal. - -2 - Also fails to filter out sixel data, leaving garbage on screen. - -3 - Latest in repository. - -4 - Requires TERM=xterm-1003 before starting. - -5 - Sixel images can crash terminal. - -6 - Version 0.4.2382.0, on Windows 10.0.18362.30. Tested against - WSL-1 Debian instance. - - - -See Also --------- - -* [Tranquil Java IDE](https://tjide.sourceforge.io) is a TUI-based - integrated development environment for the Java language that was - built using a very lightly modified GPL version of Jexer. TJ - provided a real-world use case to shake out numerous bugs and - limitations of Jexer. - -* [LCXterm](https://lcxterm.sourceforge.io) is a curses-based terminal - emulator that allows one to use Jexer with full support on the raw - Linux console. - -* [ptypipe](https://gitlab.com/klamonte/ptypipe) is a small C utility - that permits a Jexer TTerminalWindow to resize the running shell - when its window is resized. - - - -Acknowledgements ----------------- - -Jexer makes use of the Terminus TrueType font [made available -here](http://files.ax86.net/terminus-ttf/) . diff --git a/src/jexer/Scrollable.java b/Scrollable.java similarity index 100% rename from src/jexer/Scrollable.java rename to Scrollable.java diff --git a/src/jexer/TAction.java b/TAction.java similarity index 100% rename from src/jexer/TAction.java rename to TAction.java diff --git a/src/jexer/TApplication.java b/TApplication.java similarity index 90% rename from src/jexer/TApplication.java rename to TApplication.java index 9d27c10..28e3509 100644 --- a/src/jexer/TApplication.java +++ b/TApplication.java @@ -29,6 +29,7 @@ package jexer; import java.io.File; +import java.io.FileInputStream; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; @@ -47,6 +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 @@ 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 @@ public class TApplication implements Runnable { */ private Backend backend; + /** + * The clipboard for copy and paste. + */ + private Clipboard clipboard = new Clipboard(); + /** * Actual mouse coordinate X. */ @@ -158,16 +167,6 @@ public class TApplication implements Runnable { */ private int mouseY; - /** - * Old version of mouse coordinate X. - */ - private int oldMouseX; - - /** - * Old version mouse coordinate Y. - */ - private int oldMouseY; - /** * Old drawn version of mouse coordinate X. */ @@ -240,11 +239,6 @@ public class TApplication implements Runnable { */ private List windows; - /** - * The currently acive window. - */ - private TWindow activeWindow = null; - /** * Timers that are being ticked. */ @@ -325,6 +319,46 @@ public class TApplication implements Runnable { */ 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 @@ public class TApplication implements Runnable { } } + // Load the help system + invokeLater(new Runnable() { + /* + * This isn't the best solution. But basically if a TApplication + * subclass constructor throws and needs to use TExceptionDialog, + * it may end up at the bottom of the window stack with a bunch + * of modal windows on top of it if said constructors spawn their + * windows also via invokeLater(). But if they don't do that, + * and instead just conventionally construct their windows, then + * this exception dialog will end up on top where it should be. + */ + public void run() { + try { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + helpFile = new HelpFile(); + helpFile.load(loader.getResourceAsStream("help.xml")); + } catch (Exception e) { + new TExceptionDialog(TApplication.this, e); + } + } + }); } // ------------------------------------------------------------------------ @@ -904,6 +959,15 @@ public class TApplication implements Runnable { return true; } + if (command.equals(cmHelp)) { + if (getActiveWindow() != null) { + new THelpWindow(this, getActiveWindow().getHelpTopic()); + } else { + new THelpWindow(this); + } + return true; + } + if (command.equals(cmShell)) { openTerminal(0, 0, TWindow.RESIZABLE); return true; @@ -955,6 +1019,62 @@ public class TApplication implements Runnable { return true; } + if (menu.getId() == TMenu.MID_HELP_HELP) { + new THelpWindow(this, THelpWindow.HELP_HELP); + return true; + } + + if (menu.getId() == TMenu.MID_HELP_CONTENTS) { + new THelpWindow(this, helpFile.getTableOfContents()); + return true; + } + + if (menu.getId() == TMenu.MID_HELP_INDEX) { + new THelpWindow(this, helpFile.getIndex()); + return true; + } + + if (menu.getId() == TMenu.MID_HELP_SEARCH) { + TInputBox inputBox = inputBox(i18n. + getString("searchHelpInputBoxTitle"), + i18n.getString("searchHelpInputBoxCaption"), "", + TInputBox.Type.OKCANCEL); + if (inputBox.isOk()) { + new THelpWindow(this, + helpFile.getSearchResults(inputBox.getText())); + } + return true; + } + + if (menu.getId() == TMenu.MID_HELP_PREVIOUS) { + if (helpTopics.size() > 1) { + Topic previous = helpTopics.remove(helpTopics.size() - 2); + helpTopics.remove(helpTopics.size() - 1); + new THelpWindow(this, previous); + } else { + new THelpWindow(this, helpFile.getTableOfContents()); + } + return true; + } + + if (menu.getId() == TMenu.MID_HELP_ACTIVE_FILE) { + try { + List filters = new ArrayList(); + filters.add("^.*\\.[Xx][Mm][Ll]$"); + String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN, + filters); + if (filename != null) { + helpTopics = new ArrayList(); + helpFile = new HelpFile(); + helpFile.load(new FileInputStream(filename)); + } + } catch (Exception e) { + // Show this exception to the user. + new TExceptionDialog(this, e); + } + return true; + } + if (menu.getId() == TMenu.MID_SHELL) { openTerminal(0, 0, TWindow.RESIZABLE); return true; @@ -989,6 +1109,24 @@ public class TApplication implements Runnable { 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 @@ public class TApplication implements Runnable { Thread.currentThread() + " finishEventProcessing()\n"); } + // See if we need to enable/disable the edit menu. + EditMenuUser widget = null; + if (activeMenu == null) { + TWindow activeWindow = getActiveWindow(); + if (activeWindow != null) { + if (activeWindow.getActiveChild() instanceof EditMenuUser) { + widget = (EditMenuUser) activeWindow.getActiveChild(); + } + } else if (desktop != null) { + if (desktop.getActiveChild() instanceof EditMenuUser) { + widget = (EditMenuUser) desktop.getActiveChild(); + } + } + if (widget == null) { + disableMenuItem(TMenu.MID_CUT); + disableMenuItem(TMenu.MID_COPY); + disableMenuItem(TMenu.MID_PASTE); + disableMenuItem(TMenu.MID_CLEAR); + } else { + if (widget.isEditMenuCut()) { + enableMenuItem(TMenu.MID_CUT); + } else { + disableMenuItem(TMenu.MID_CUT); + } + if (widget.isEditMenuCopy()) { + enableMenuItem(TMenu.MID_COPY); + } else { + disableMenuItem(TMenu.MID_COPY); + } + if (widget.isEditMenuPaste()) { + enableMenuItem(TMenu.MID_PASTE); + } else { + disableMenuItem(TMenu.MID_PASTE); + } + if (widget.isEditMenuClear()) { + enableMenuItem(TMenu.MID_CLEAR); + } else { + disableMenuItem(TMenu.MID_CLEAR); + } + } + } + // Process timers and call doIdle()'s doIdle(); @@ -1101,8 +1281,6 @@ public class TApplication implements Runnable { } mouseX = 0; mouseY = 0; - oldMouseX = 0; - oldMouseY = 0; } if (desktop != null) { desktop.setDimensions(0, desktopTop, resize.getWidth(), @@ -1157,9 +1335,29 @@ public class TApplication implements Runnable { 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 @@ public class TApplication implements Runnable { mouse.getAbsoluteX(), mouse.getAbsoluteY(), mouse.isMouse1(), mouse.isMouse2(), mouse.isMouse3(), - mouse.isMouseWheelUp(), mouse.isMouseWheelDown()); + mouse.isMouseWheelUp(), mouse.isMouseWheelDown(), + mouse.isAlt(), mouse.isCtrl(), mouse.isShift()); } else { // The first click of a potential double-click. @@ -1235,6 +1434,7 @@ public class TApplication implements Runnable { // shortcutted by the active window, and if so dispatch the menu // event. boolean windowWillShortcut = false; + TWindow activeWindow = getActiveWindow(); if (activeWindow != null) { assert (activeWindow.isShown()); if (activeWindow.isShortcutKeypress(keypress.getKey())) { @@ -1279,7 +1479,7 @@ public class TApplication implements Runnable { // Dispatch events to the active window ------------------------------- boolean dispatchToDesktop = true; - TWindow window = activeWindow; + TWindow window = getActiveWindow(); if (window != null) { assert (window.isActive()); assert (window.isShown()); @@ -1347,8 +1547,6 @@ public class TApplication implements Runnable { 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 @@ public class TApplication implements Runnable { mouse.getAbsoluteX(), mouse.getAbsoluteY(), mouse.isMouse1(), mouse.isMouse2(), mouse.isMouse3(), - mouse.isMouseWheelUp(), mouse.isMouseWheelDown()); + mouse.isMouseWheelUp(), mouse.isMouseWheelDown(), + mouse.isAlt(), mouse.isCtrl(), mouse.isShift()); } else { // The first click of a potential double-click. @@ -1474,13 +1673,17 @@ public class TApplication implements Runnable { desktop.onIdle(); } - // Run any invokeLaters + // Run any invokeLaters. We make a copy, and run that, because one + // of these Runnables might add call TApplication.invokeLater(). + List invokes = new ArrayList(); synchronized (invokeLaters) { - for (Runnable invoke: invokeLaters) { - invoke.run(); - } + invokes.addAll(invokeLaters); invokeLaters.clear(); } + for (Runnable invoke: invokes) { + invoke.run(); + } + doRepaint(); } @@ -1582,6 +1785,15 @@ public class TApplication implements Runnable { return theme; } + /** + * Get the clipboard. + * + * @return the clipboard + */ + public final Clipboard getClipboard() { + return clipboard; + } + /** * Repaint the screen on the next update. */ @@ -1638,7 +1850,12 @@ public class TApplication implements Runnable { * @return the active window, or null if it is not set */ public final TWindow getActiveWindow() { - return activeWindow; + for (TWindow window: windows) { + if (window.isShown() && window.isActive()) { + return window; + } + } + return null; } /** @@ -1679,7 +1896,7 @@ public class TApplication implements Runnable { String version = getClass().getPackage().getImplementationVersion(); if (version == null) { // This is Java 9+, use a hardcoded string here. - version = "0.3.2"; + version = "1.0.0"; } messageBox(i18n.getString("aboutDialogTitle"), MessageFormat.format(i18n.getString("aboutDialogText"), version), @@ -1724,27 +1941,16 @@ public class TApplication implements Runnable { // ------------------------------------------------------------------------ /** - * 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 @@ public class TApplication implements Runnable { } } - 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 @@ public class TApplication implements Runnable { } } + 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 @@ public class TApplication implements Runnable { // 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 @@ public class TApplication implements Runnable { 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 @@ public class TApplication implements Runnable { * * @param window the window to become the new active window */ - public void activateWindow(final TWindow window) { + public final void activateWindow(final TWindow window) { if (hasWindow(window) == false) { /* * Someone has a handle to a window I don't have. Ignore this @@ -2163,68 +2346,61 @@ public class TApplication implements Runnable { return; } - // Whatever window might be moving/dragging, stop it now. - for (TWindow w: windows) { - if (w.inMovements()) { - w.stopMovements(); - } + 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 @@ public class TApplication implements Runnable { return; } - // Whatever window might be moving/dragging, stop it now. - for (TWindow w: windows) { - if (w.inMovements()) { - w.stopMovements(); + synchronized (windows) { + + // Whatever window might be moving/dragging, stop it now. + for (TWindow w: windows) { + if (w.inMovements()) { + w.stopMovements(); + } } - } - assert (windows.size() > 0); + assert (windows.size() > 0); - if (!window.hidden) { - if (window == activeWindow) { - if (shownWindowCount() > 1) { - switchWindow(true); - } else { - activeWindow = null; - window.setActive(false); - window.onUnfocus(); - } + if (window.hidden) { + return; } + + window.setActive(false); window.hidden = true; window.onHide(); - } + + TWindow activeWindow = null; + for (TWindow w: windows) { + if (w.isShown()) { + activeWindow = w; + break; + } + } + assert (activeWindow != window); + if (activeWindow != null) { + activateWindow(activeWindow); + } + + } // synchronized (windows) + } /** @@ -2280,25 +2467,16 @@ public class TApplication implements Runnable { return; } - // Whatever window might be moving/dragging, stop it now. - for (TWindow w: windows) { - if (w.inMovements()) { - w.stopMovements(); - } - } - - assert (windows.size() > 0); - if (window.hidden) { window.hidden = false; window.onShow(); activateWindow(window); } + } /** - * Close window. Note that the window's destructor is NOT called by this - * method, instead the GC is assumed to do the cleanup. + * Close window. * * @param window the window to remove */ @@ -2316,23 +2494,16 @@ public class TApplication implements Runnable { window.onPreClose(); synchronized (windows) { - // Whatever window might be moving/dragging, stop it now. - for (TWindow w: windows) { - if (w.inMovements()) { - w.stopMovements(); - } - } - int z = window.getZ(); - window.setZ(-1); + window.stopMovements(); window.onUnfocus(); windows.remove(window); Collections.sort(windows); - activeWindow = null; - int newZ = 0; - boolean foundNextWindow = false; + TWindow nextWindow = null; + int newZ = 0; for (TWindow w: windows) { + w.stopMovements(); w.setZ(newZ); newZ++; @@ -2340,22 +2511,22 @@ public class TApplication implements Runnable { if (w.isHidden()) { continue; } - - if (foundNextWindow == false) { - foundNextWindow = true; - w.setActive(true); - w.onFocus(); - assert (activeWindow == null); - activeWindow = w; - continue; + if (nextWindow == null) { + nextWindow = w; + } else { + if (w.isActive()) { + w.setActive(false); + w.onUnfocus(); + } } + } - if (w.isActive()) { - w.setActive(false); - w.onUnfocus(); - } + if (nextWindow != null) { + nextWindow.setActive(true); + nextWindow.onFocus(); } - } + + } // synchronized (windows) // Perform window cleanup window.onClose(); @@ -2373,7 +2544,8 @@ public class TApplication implements Runnable { synchronized (secondaryEventHandler) { secondaryEventHandler.notify(); } - } + + } // synchronized (windows) // Permit desktop to be active if it is the only thing left. if (desktop != null) { @@ -2394,53 +2566,50 @@ public class TApplication implements Runnable { if (shownWindowCount() < 2) { return; } - assert (activeWindow != null); + + if (modalWindowActive()) { + // Do not switch if a window is modal + return; + } synchronized (windows) { - // Whatever window might be moving/dragging, stop it now. - for (TWindow w: windows) { - if (w.inMovements()) { - w.stopMovements(); - } - } - // Swap z/active between active window and the next in the list - int activeWindowI = -1; - for (int i = 0; i < windows.size(); i++) { - if (windows.get(i) == activeWindow) { - assert (activeWindow.isActive()); - activeWindowI = i; - break; + TWindow window = windows.get(0); + do { + assert (window != null); + if (forward) { + window.setZ(windows.size()); } else { - assert (!windows.get(0).isActive()); + TWindow lastWindow = windows.get(windows.size() - 1); + lastWindow.setZ(-1); } - } - assert (activeWindowI >= 0); - - // Do not switch if a window is modal - if (activeWindow.isModal()) { - return; - } - int nextWindowI = activeWindowI; - for (;;) { - if (forward) { - nextWindowI++; - nextWindowI %= windows.size(); - } else { - nextWindowI--; - if (nextWindowI < 0) { - nextWindowI = windows.size() - 1; - } + Collections.sort(windows); + int newZ = 0; + for (TWindow w: windows) { + w.setZ(newZ); + newZ++; } - if (windows.get(nextWindowI).isShown()) { - activateWindow(windows.get(nextWindowI)); - break; + window = windows.get(0); + } while (!window.isShown()); + + // The next visible window is now on top. Renumber the list. + for (TWindow w: windows) { + w.stopMovements(); + if ((w != window) && w.isActive()) { + assert (w.isShown()); + w.setActive(false); + w.onUnfocus(); } } - } // synchronized (windows) + // Next visible window is on top. + assert (window.isShown()); + window.setActive(true); + window.onFocus(); + + } // synchronized (windows) } /** @@ -2490,13 +2659,13 @@ public class TApplication implements Runnable { } w.setZ(w.getZ() + 1); } - } - windows.add(window); - if (window.isShown()) { - activeWindow = window; - activeWindow.setZ(0); - activeWindow.setActive(true); - activeWindow.onFocus(); + window.setZ(0); + window.setActive(true); + window.onFocus(); + windows.add(0, window); + } else { + window.setZ(windows.size()); + windows.add(window); } if (((window.flags & TWindow.CENTERED) == 0) @@ -2513,6 +2682,7 @@ public class TApplication implements Runnable { if (desktop != null) { desktop.setActive(false); } + } /** @@ -2540,6 +2710,7 @@ public class TApplication implements Runnable { * @return true if the active window is overriding the menu */ private boolean overrideMenuWindowActive() { + TWindow activeWindow = getActiveWindow(); if (activeWindow != null) { if (activeWindow.hasOverriddenMenu()) { return true; @@ -2910,7 +3081,6 @@ public class TApplication implements Runnable { || (mouse.getType() == TMouseEvent.Type.MOUSE_DOWN) ) { synchronized (windows) { - Collections.sort(windows); if (windows.get(0).isModal()) { // Modal windows don't switch return; @@ -2925,25 +3095,7 @@ public class TApplication implements Runnable { } if (window.mouseWouldHit(mouse)) { - if (window == windows.get(0)) { - // Clicked on the same window, nothing to do - assert (window.isActive()); - return; - } - - // We will be switching to another window - assert (windows.get(0).isActive()); - assert (windows.get(0) == activeWindow); - assert (!window.isActive()); - if (activeWindow != null) { - activeWindow.onUnfocus(); - activeWindow.setActive(false); - activeWindow.setZ(window.getZ()); - } - activeWindow = window; - window.setZ(0); - window.setActive(true); - window.onFocus(); + activateWindow(window); return; } } @@ -3278,10 +3430,13 @@ public class TApplication implements Runnable { */ public final TMenu addEditMenu() { TMenu editMenu = addMenu(i18n.getString("editMenuTitle")); - editMenu.addDefaultItem(TMenu.MID_CUT); - editMenu.addDefaultItem(TMenu.MID_COPY); - editMenu.addDefaultItem(TMenu.MID_PASTE); - editMenu.addDefaultItem(TMenu.MID_CLEAR); + editMenu.addDefaultItem(TMenu.MID_UNDO, false); + editMenu.addDefaultItem(TMenu.MID_REDO, false); + editMenu.addSeparator(); + editMenu.addDefaultItem(TMenu.MID_CUT, false); + editMenu.addDefaultItem(TMenu.MID_COPY, false); + editMenu.addDefaultItem(TMenu.MID_PASTE, false); + editMenu.addDefaultItem(TMenu.MID_CLEAR, false); TStatusBar statusBar = editMenu.newStatusBar(i18n. getString("editMenuStatus")); statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help")); diff --git a/src/jexer/TApplication.properties b/TApplication.properties similarity index 87% rename from src/jexer/TApplication.properties rename to TApplication.properties index 299c6a3..57f7c59 100644 --- a/src/jexer/TApplication.properties +++ b/TApplication.properties @@ -25,3 +25,6 @@ exitDialogText=Exit application? aboutDialogTitle=About aboutDialogText=Jexer Version {0} + +searchHelpInputBoxTitle=Search Help Topics +searchHelpInputBoxCaption=Search help topics for (regex): diff --git a/src/jexer/TButton.java b/TButton.java similarity index 97% rename from src/jexer/TButton.java rename to TButton.java index d86fa44..d1d7b39 100644 --- a/src/jexer/TButton.java +++ b/TButton.java @@ -129,6 +129,20 @@ public class TButton extends TWidget { this(parent, text, x, y); this.action = action; } + + /** + * The action to call when the button is pressed. + **/ + public TAction getAction() { + return action; + } + + /** + * The action to call when the button is pressed. + **/ + public void setAction(TAction action) { + this.action = action; + } // ------------------------------------------------------------------------ // Event handlers --------------------------------------------------------- diff --git a/src/jexer/TCalendar.java b/TCalendar.java similarity index 100% rename from src/jexer/TCalendar.java rename to TCalendar.java diff --git a/src/jexer/TCheckBox.java b/TCheckBox.java similarity index 100% rename from src/jexer/TCheckBox.java rename to TCheckBox.java diff --git a/src/jexer/TComboBox.java b/TComboBox.java similarity index 100% rename from src/jexer/TComboBox.java rename to TComboBox.java diff --git a/src/jexer/TCommand.java b/TCommand.java similarity index 100% rename from src/jexer/TCommand.java rename to TCommand.java diff --git a/src/jexer/TDesktop.java b/TDesktop.java similarity index 100% rename from src/jexer/TDesktop.java rename to TDesktop.java diff --git a/src/jexer/TDirectoryList.java b/TDirectoryList.java similarity index 100% rename from src/jexer/TDirectoryList.java rename to TDirectoryList.java diff --git a/src/jexer/TEditColorThemeWindow.java b/TEditColorThemeWindow.java similarity index 100% rename from src/jexer/TEditColorThemeWindow.java rename to TEditColorThemeWindow.java diff --git a/src/jexer/TEditColorThemeWindow.properties b/TEditColorThemeWindow.properties similarity index 100% rename from src/jexer/TEditColorThemeWindow.properties rename to TEditColorThemeWindow.properties diff --git a/TEditorWidget.java b/TEditorWidget.java new file mode 100644 index 0000000..bea25ed --- /dev/null +++ b/TEditorWidget.java @@ -0,0 +1,1441 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +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 implements EditMenuUser { + + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The number of lines to scroll on mouse wheel up/down. + */ + private static final int wheelScrollSize = 3; + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The document being edited. + */ + protected Document document; + + /** + * The default color for the editable text. + */ + private CellAttributes defaultColor = null; + + /** + * The topmost line number in the visible area. 0-based. + */ + private int topLine = 0; + + /** + * The leftmost column number in the visible area. 0-based. + */ + 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 ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + * + * @param parent parent widget + * @param text text on the screen + * @param x column relative to parent + * @param y row relative to parent + * @param width width of text area + * @param height height of text area + */ + public TEditorWidget(final TWidget parent, final String text, final int x, + final int y, final int width, final int height) { + + // Set parent and window + super(parent, x, y, width, height); + + setCursorVisible(true); + + defaultColor = getTheme().getColor("teditor"); + document = new Document(text, defaultColor); + } + + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Handle mouse press events. + * + * @param mouse mouse button press event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + if (mouse.isMouseWheelUp()) { + for (int i = 0; i < wheelScrollSize; i++) { + if (topLine > 0) { + topLine--; + alignDocument(false); + } + } + return; + } + if (mouse.isMouseWheelDown()) { + for (int i = 0; i < wheelScrollSize; i++) { + if (topLine < document.getLineCount() - 1) { + topLine++; + alignDocument(true); + } + } + return; + } + + if (mouse.isMouse1()) { + // 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); + 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; + } 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; + } + + // Pass to children + super.onMouseDown(mouse); + } + + /** + * Handle keystrokes. + * + * @param keypress keystroke event + */ + @Override + public void onKeypress(final TKeypressEvent keypress) { + 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) + || 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) + || keypress.equals(kbShiftUp) + ) { + document.up(); + alignTopLine(false); + } else if (keypress.equals(kbDown) + || keypress.equals(kbShiftDown) + ) { + document.down(); + alignTopLine(true); + } else if (keypress.equals(kbPgUp) + || keypress.equals(kbShiftPgUp) + ) { + document.up(getHeight() - 1); + alignTopLine(false); + } else if (keypress.equals(kbPgDn) + || keypress.equals(kbShiftPgDn) + ) { + document.down(getHeight() - 1); + alignTopLine(true); + } else if (keypress.equals(kbHome) + || keypress.equals(kbShiftHome) + ) { + if (document.home()) { + leftColumn = 0; + if (leftColumn < 0) { + leftColumn = 0; + } + setCursorX(0); + } + } else if (keypress.equals(kbEnd) + || keypress.equals(kbShiftEnd) + ) { + if (document.end()) { + alignCursor(); + } + } 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) + || keypress.equals(kbCtrlShiftEnd) + ) { + document.setLineNumber(document.getLineCount() - 1); + document.end(); + alignTopLine(false); + } else if (keypress.equals(kbIns)) { + document.setOverwrite(!document.isOverwrite()); + } else if (keypress.equals(kbDel)) { + if (inSelection) { + deleteSelection(); + alignCursor(); + } else { + saveUndo(); + document.del(); + alignCursor(); + } + } else if (keypress.equals(kbBackspace) + || keypress.equals(kbBackspaceDel) + ) { + 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() + && !keypress.getKey().isAlt() + && !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(); + } + } + + /** + * Method that subclasses can override to handle window/screen resize + * events. + * + * @param resize resize event + */ + @Override + public void onResize(final TResizeEvent resize) { + // Change my width/height, and pull the cursor in as needed. + if (resize.getType() == TResizeEvent.Type.WIDGET) { + setWidth(resize.getWidth()); + setHeight(resize.getHeight()); + // See if the cursor is now outside the window, and if so move + // things. + if (getCursorX() >= getWidth()) { + leftColumn += getCursorX() - (getWidth() - 1); + setCursorX(getWidth() - 1); + } + if (getCursorY() >= getHeight()) { + topLine += getCursorY() - (getHeight() - 1); + setCursorY(getHeight() - 1); + } + } else { + // Let superclass handle it + super.onResize(resize); + } + } + + /** + * 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. + * + * @param topLineIsTop if true, make the top visible line the document + * current line if it was off-screen. If false, make the bottom visible + * line the document current line. + */ + private void alignTopLine(final boolean topLineIsTop) { + int line = document.getLineNumber(); + + if ((line < topLine) || (line > topLine + getHeight() - 1)) { + // Need to move topLine to bring document back into view. + if (topLineIsTop) { + topLine = line - (getHeight() - 1); + if (topLine < 0) { + topLine = 0; + } + assert (topLine >= 0); + } else { + topLine = line; + assert (topLine >= 0); + } + } + + /* + System.err.println("line " + line + " topLine " + topLine); + */ + + // Document is in view, let's set cursorY + assert (line >= topLine); + setCursorY(line - topLine); + alignCursor(); + } + + /** + * Align document current line with visible area. + * + * @param topLineIsTop if true, make the top visible line the document + * current line if it was off-screen. If false, make the bottom visible + * line the document current line. + */ + private void alignDocument(final boolean topLineIsTop) { + int line = document.getLineNumber(); + int cursor = document.getCursor(); + + if ((line < topLine) || (line > topLine + getHeight() - 1)) { + // Need to move document to ensure it fits view. + if (topLineIsTop) { + document.setLineNumber(topLine); + } else { + document.setLineNumber(topLine + (getHeight() - 1)); + } + if (cursor < document.getCurrentLine().getDisplayLength()) { + document.setCursor(cursor); + } + } + + /* + System.err.println("getLineNumber() " + document.getLineNumber() + + " topLine " + topLine); + */ + + // Document is in view, let's set cursorY + setCursorY(document.getLineNumber() - topLine); + alignCursor(); + } + + /** + * Align visible cursor with document cursor. + */ + private void alignCursor() { + int width = getWidth(); + + int desiredX = document.getCursor() - leftColumn; + if (desiredX < 0) { + // We need to push the screen to the left. + leftColumn = document.getCursor(); + } else if (desiredX > width - 1) { + // We need to push the screen to the right. + leftColumn = document.getCursor() - (width - 1); + } + + /* + System.err.println("document cursor " + document.getCursor() + + " leftColumn " + leftColumn); + */ + + + setCursorX(document.getCursor() - leftColumn); + } + + /** + * Get the number of lines in the underlying Document. + * + * @return the number of lines + */ + public int getLineCount() { + return document.getLineCount(); + } + + /** + * Get the current visible top row number. 1-based. + * + * @return the visible top row number. Row 1 is the first row. + */ + public int getVisibleRowNumber() { + return topLine + 1; + } + + /** + * Set the current visible row number. 1-based. + * + * @param row the new visible row number. Row 1 is the first row. + */ + public void setVisibleRowNumber(final int row) { + assert (row > 0); + if ((row > 0) && (row < document.getLineCount())) { + topLine = row - 1; + alignDocument(true); + } + } + + /** + * Get the current editing row number. 1-based. + * + * @return the editing row number. Row 1 is the first row. + */ + public int getEditingRowNumber() { + return document.getLineNumber() + 1; + } + + /** + * Set the current editing row number. 1-based. + * + * @param row the new editing row number. Row 1 is the first row. + */ + public void setEditingRowNumber(final int row) { + assert (row > 0); + if ((row > 0) && (row < document.getLineCount())) { + document.setLineNumber(row - 1); + alignTopLine(true); + } + } + + /** + * Set the current visible column number. 1-based. + * + * @return the visible column number. Column 1 is the first column. + */ + public int getVisibleColumnNumber() { + return leftColumn + 1; + } + + /** + * Set the current visible column number. 1-based. + * + * @param column the new visible column number. Column 1 is the first + * column. + */ + public void setVisibleColumnNumber(final int column) { + assert (column > 0); + if ((column > 0) && (column < document.getLineLengthMax())) { + leftColumn = column - 1; + alignDocument(true); + } + } + + /** + * Get the current editing column number. 1-based. + * + * @return the editing column number. Column 1 is the first column. + */ + public int getEditingColumnNumber() { + return document.getCursor() + 1; + } + + /** + * Set the current editing column number. 1-based. + * + * @param column the new editing column number. Column 1 is the first + * column. + */ + public void setEditingColumnNumber(final int column) { + if ((column > 0) && (column < document.getLineLength())) { + document.setCursor(column - 1); + alignCursor(); + } + } + + /** + * Get the maximum possible row number. 1-based. + * + * @return the maximum row number. Row 1 is the first row. + */ + public int getMaximumRowNumber() { + return document.getLineCount() + 1; + } + + /** + * Get the maximum possible column number. 1-based. + * + * @return the maximum column number. Column 1 is the first column. + */ + public int getMaximumColumnNumber() { + 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. + * + * @return true if the buffer is dirty + */ + public boolean isDirty() { + 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. + * + * @param filename file to save to + * @throws IOException if a java.io operation throws + */ + public void saveToFilename(final String filename) throws IOException { + 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 --git a/src/jexer/TEditorWindow.java b/TEditorWindow.java similarity index 92% rename from src/jexer/TEditorWindow.java rename to TEditorWindow.java index d78185c..a28376b 100644 --- a/src/jexer/TEditorWindow.java +++ b/TEditorWindow.java @@ -44,8 +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 @@ public class TEditorWindow extends TScrollableWindow { } // ------------------------------------------------------------------------ - // 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 @@ public class TEditorWindow extends TScrollableWindow { 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 --git a/src/jexer/TEditorWindow.properties b/TEditorWindow.properties similarity index 100% rename from src/jexer/TEditorWindow.properties rename to TEditorWindow.properties diff --git a/src/jexer/TExceptionDialog.java b/TExceptionDialog.java similarity index 97% rename from src/jexer/TExceptionDialog.java rename to TExceptionDialog.java index 227aceb..f526a64 100644 --- a/src/jexer/TExceptionDialog.java +++ b/TExceptionDialog.java @@ -82,7 +82,7 @@ public class TExceptionDialog extends TWindow { 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 @@ public class TExceptionDialog extends TWindow { 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 @@ public class TExceptionDialog extends TWindow { }); 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 --git a/src/jexer/TExceptionDialog.properties b/TExceptionDialog.properties similarity index 52% rename from src/jexer/TExceptionDialog.properties rename to TExceptionDialog.properties index d07998c..9e5857a 100644 --- a/src/jexer/TExceptionDialog.properties +++ b/TExceptionDialog.properties @@ -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 --git a/src/jexer/TField.java b/TField.java similarity index 90% rename from src/jexer/TField.java rename to TField.java index 7c8b5bc..90dd4e4 100644 --- a/src/jexer/TField.java +++ b/TField.java @@ -31,14 +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 @@ public class TField extends TWidget { 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 @@ public class TField extends TWidget { 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 @@ public class TField extends TWidget { 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 @@ public class TField extends TWidget { 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 --git a/src/jexer/TFileOpenBox.java b/TFileOpenBox.java similarity index 100% rename from src/jexer/TFileOpenBox.java rename to TFileOpenBox.java diff --git a/src/jexer/TFileOpenBox.properties b/TFileOpenBox.properties similarity index 100% rename from src/jexer/TFileOpenBox.properties rename to TFileOpenBox.properties diff --git a/src/jexer/TFontChooserWindow.java b/TFontChooserWindow.java similarity index 100% rename from src/jexer/TFontChooserWindow.java rename to TFontChooserWindow.java diff --git a/src/jexer/TFontChooserWindow.properties b/TFontChooserWindow.properties similarity index 100% rename from src/jexer/TFontChooserWindow.properties rename to TFontChooserWindow.properties diff --git a/src/jexer/THScroller.java b/THScroller.java similarity index 100% rename from src/jexer/THScroller.java rename to THScroller.java diff --git a/THelpWindow.java b/THelpWindow.java new file mode 100644 index 0000000..ee7ce54 --- /dev/null +++ b/THelpWindow.java @@ -0,0 +1,271 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer; + +import java.util.ResourceBundle; + +import jexer.bits.CellAttributes; +import jexer.event.TResizeEvent; +import jexer.help.THelpText; +import jexer.help.Topic; + +/** + * THelpWindow + */ +public class THelpWindow extends TWindow { + + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(THelpWindow.class.getName()); + + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + // Default help topic keys. Note package private access. + static String HELP_HELP = "Help On Help"; + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The help text window. + */ + private THelpText helpText; + + /** + * The "Contents" button. + */ + private TButton contentsButton; + + /** + * The "Index" button. + */ + private TButton indexButton; + + /** + * The "Previous" button. + */ + private TButton previousButton; + + /** + * The "Close" button. + */ + private TButton closeButton; + + /** + * The X position for the buttons. + */ + private int buttonOffset = 14; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + * + * @param application TApplication that manages this window + * @param topic the topic to start on + */ + public THelpWindow(final TApplication application, final String topic) { + this (application, application.helpFile.getTopic(topic)); + } + + /** + * Public constructor. + * + * @param application TApplication that manages this window + * @param topic the topic to start on + */ + public THelpWindow(final TApplication application, final Topic topic) { + super(application, i18n.getString("windowTitle"), + 1, 1, 78, 22, CENTERED | RESIZABLE); + + setMinimumWindowHeight(16); + setMinimumWindowWidth(30); + + helpText = new THelpText(this, topic, 1, 1, + getWidth() - buttonOffset - 4, getHeight() - 4); + + setHelpTopic(topic); + + // Buttons + previousButton = addButton(i18n.getString("previousButton"), + getWidth() - buttonOffset, 4, + new TAction() { + public void DO() { + if (application.helpTopics.size() > 1) { + Topic previous = application.helpTopics.remove( + application.helpTopics.size() - 2); + application.helpTopics.remove(application. + helpTopics.size() - 1); + setHelpTopic(previous); + } + } + }); + + contentsButton = addButton(i18n.getString("contentsButton"), + getWidth() - buttonOffset, 6, + new TAction() { + public void DO() { + setHelpTopic(application.helpFile.getTableOfContents()); + } + }); + + indexButton = addButton(i18n.getString("indexButton"), + getWidth() - buttonOffset, 8, + new TAction() { + public void DO() { + setHelpTopic(application.helpFile.getIndex()); + } + }); + + closeButton = addButton(i18n.getString("closeButton"), + getWidth() - buttonOffset, 10, + new TAction() { + public void DO() { + // Don't copy anything, just close the window. + THelpWindow.this.close(); + } + }); + + // Save this for last: make the close button default action. + activate(closeButton); + + } + + /** + * Public constructor. + * + * @param application TApplication that manages this window + */ + public THelpWindow(final TApplication application) { + this(application, HELP_HELP); + } + + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Handle window/screen resize events. + * + * @param event resize event + */ + @Override + public void onResize(final TResizeEvent event) { + if (event.getType() == TResizeEvent.Type.WIDGET) { + + previousButton.setX(getWidth() - buttonOffset); + contentsButton.setX(getWidth() - buttonOffset); + indexButton.setX(getWidth() - buttonOffset); + closeButton.setX(getWidth() - buttonOffset); + + helpText.setDimensions(1, 1, getWidth() - buttonOffset - 4, + getHeight() - 4); + helpText.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, + helpText.getWidth(), helpText.getHeight())); + + return; + } else { + super.onResize(event); + } + } + + // ------------------------------------------------------------------------ + // TWindow ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Retrieve the background color. + * + * @return the background color + */ + @Override + public final CellAttributes getBackground() { + return getTheme().getColor("thelpwindow.background"); + } + + /** + * Retrieve the border color. + * + * @return the border color + */ + @Override + public CellAttributes getBorder() { + if (inWindowMove) { + return getTheme().getColor("thelpwindow.windowmove"); + } + return getTheme().getColor("thelpwindow.background"); + } + + /** + * Retrieve the color used by the window movement/sizing controls. + * + * @return the color used by the zoom box, resize bar, and close box + */ + @Override + public CellAttributes getBorderControls() { + return getTheme().getColor("thelpwindow.border"); + } + + // ------------------------------------------------------------------------ + // THelpWindow ------------------------------------------------------------ + // ------------------------------------------------------------------------ + + /** + * Set the topic to display. + * + * @param topic the topic to display + */ + public void setHelpTopic(final String topic) { + setHelpTopic(getApplication().helpFile.getTopic(topic)); + } + + /** + * Set the topic to display. + * + * @param topic the topic to display + */ + private void setHelpTopic(final Topic topic) { + boolean separator = true; + if ((topic == getApplication().helpFile.getTableOfContents()) + || (topic == getApplication().helpFile.getIndex()) + ) { + separator = false; + } + + getApplication().helpTopics.add(topic); + helpText.setTopic(topic, separator); + } + +} diff --git a/THelpWindow.properties b/THelpWindow.properties new file mode 100644 index 0000000..2b25484 --- /dev/null +++ b/THelpWindow.properties @@ -0,0 +1,5 @@ +windowTitle=Help +previousButton=Pre&vious +contentsButton=Co&ntents +indexButton=\ &Index\ \ +closeButton=\ C&lose\ \ diff --git a/src/jexer/TImage.java b/TImage.java similarity index 90% rename from src/jexer/TImage.java rename to TImage.java index cd0ce96..b7bfbd0 100644 --- a/src/jexer/TImage.java +++ b/TImage.java @@ -30,19 +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 @@ public class TImage extends TWidget { 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 @@ public class TImage extends TWidget { } 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 @@ public class TImage extends TWidget { 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 --git a/src/jexer/TImageWindow.java b/TImageWindow.java similarity index 100% rename from src/jexer/TImageWindow.java rename to TImageWindow.java diff --git a/src/jexer/TImageWindow.properties b/TImageWindow.properties similarity index 100% rename from src/jexer/TImageWindow.properties rename to TImageWindow.properties diff --git a/src/jexer/TInputBox.java b/TInputBox.java similarity index 100% rename from src/jexer/TInputBox.java rename to TInputBox.java diff --git a/src/jexer/TKeypress.java b/TKeypress.java similarity index 95% rename from src/jexer/TKeypress.java rename to TKeypress.java index c965e7d..20db8bb 100644 --- a/src/jexer/TKeypress.java +++ b/TKeypress.java @@ -612,6 +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 @@ public class TKeypress { return "\u25C0\u2500\u2518"; } + // Special case: Space is "Space" + if (equals(kbSpace)) { + return "Space"; + } + if (equals(kbShiftLeft)) { return "Shift+\u2190"; } diff --git a/src/jexer/TLabel.java b/TLabel.java similarity index 100% rename from src/jexer/TLabel.java rename to TLabel.java diff --git a/src/jexer/TList.java b/TList.java similarity index 97% rename from src/jexer/TList.java rename to TList.java index 38a994c..12e0b8a 100644 --- a/src/jexer/TList.java +++ b/TList.java @@ -335,21 +335,28 @@ public class TList extends TScrollableWidget { @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 @@ public class TList extends TScrollableWidget { 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 --git a/src/jexer/TMessageBox.java b/TMessageBox.java similarity index 100% rename from src/jexer/TMessageBox.java rename to TMessageBox.java diff --git a/src/jexer/TMessageBox.properties b/TMessageBox.properties similarity index 100% rename from src/jexer/TMessageBox.properties rename to TMessageBox.properties diff --git a/src/jexer/TPanel.java b/TPanel.java similarity index 100% rename from src/jexer/TPanel.java rename to TPanel.java diff --git a/src/jexer/TPasswordField.java b/TPasswordField.java similarity index 99% rename from src/jexer/TPasswordField.java rename to TPasswordField.java index 9c200d7..0be2b98 100644 --- a/src/jexer/TPasswordField.java +++ b/TPasswordField.java @@ -29,7 +29,6 @@ package jexer; import jexer.bits.CellAttributes; -import jexer.bits.GraphicsChars; import jexer.bits.StringUtils; /** diff --git a/src/jexer/TProgressBar.java b/TProgressBar.java similarity index 100% rename from src/jexer/TProgressBar.java rename to TProgressBar.java diff --git a/src/jexer/TRadioButton.java b/TRadioButton.java similarity index 91% rename from src/jexer/TRadioButton.java rename to TRadioButton.java index 60a6288..dcc5c13 100644 --- a/src/jexer/TRadioButton.java +++ b/TRadioButton.java @@ -50,9 +50,9 @@ public class TRadioButton extends TWidget { // ------------------------------------------------------------------------ /** - * 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 @@ public class TRadioButton extends TWidget { /** * 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 @@ public class TRadioButton extends TWidget { * @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 @@ public class TRadioButton extends TWidget { setCursorVisible(true); setCursorX(1); + + parent.addRadioButton(this); } // ------------------------------------------------------------------------ @@ -120,8 +122,7 @@ public class TRadioButton extends TWidget { 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 @@ public class TRadioButton extends TWidget { 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 @@ public class TRadioButton extends TWidget { } /** - * 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 --git a/src/jexer/TRadioGroup.java b/TRadioGroup.java similarity index 69% rename from src/jexer/TRadioGroup.java rename to TRadioGroup.java index a82b074..d6bd7ff 100644 --- a/src/jexer/TRadioGroup.java +++ b/TRadioGroup.java @@ -54,12 +54,30 @@ public class TRadioGroup extends TWidget { * 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 @@ public class TRadioGroup extends TWidget { 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 @@ public class TRadioGroup extends TWidget { 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 @@ public class TRadioGroup extends TWidget { * @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 @@ public class TRadioGroup extends TWidget { // 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 --git a/src/jexer/TScrollableWidget.java b/TScrollableWidget.java similarity index 100% rename from src/jexer/TScrollableWidget.java rename to TScrollableWidget.java diff --git a/src/jexer/TScrollableWindow.java b/TScrollableWindow.java similarity index 100% rename from src/jexer/TScrollableWindow.java rename to TScrollableWindow.java diff --git a/src/jexer/TSpinner.java b/TSpinner.java similarity index 100% rename from src/jexer/TSpinner.java rename to TSpinner.java diff --git a/src/jexer/TSplitPane.java b/TSplitPane.java similarity index 90% rename from src/jexer/TSplitPane.java rename to TSplitPane.java index 7c85278..b308e9b 100644 --- a/src/jexer/TSplitPane.java +++ b/TSplitPane.java @@ -30,10 +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 @@ public class TSplitPane extends TWidget { 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 @@ public class TSplitPane extends TWidget { } } 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 @@ public class TSplitPane extends TWidget { keep.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(), getHeight())); } - + return keep; } diff --git a/src/jexer/TStatusBar.java b/TStatusBar.java similarity index 100% rename from src/jexer/TStatusBar.java rename to TStatusBar.java diff --git a/src/jexer/TTableWidget.java b/TTableWidget.java similarity index 99% rename from src/jexer/TTableWidget.java rename to TTableWidget.java index 9b4d7c9..749b731 100644 --- a/src/jexer/TTableWidget.java +++ b/TTableWidget.java @@ -1426,8 +1426,6 @@ public class TTableWidget extends TWidget { 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 --git a/src/jexer/TTableWindow.java b/TTableWindow.java similarity index 99% rename from src/jexer/TTableWindow.java rename to TTableWindow.java index 44ff7b4..766ceaf 100644 --- a/src/jexer/TTableWindow.java +++ b/TTableWindow.java @@ -112,7 +112,6 @@ public class TTableWindow extends TScrollableWindow { */ 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 @@ public class TTableWindow extends TScrollableWindow { */ 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 --git a/src/jexer/TTableWindow.properties b/TTableWindow.properties similarity index 100% rename from src/jexer/TTableWindow.properties rename to TTableWindow.properties diff --git a/src/jexer/TTerminalWidget.java b/TTerminalWidget.java similarity index 85% rename from src/jexer/TTerminalWidget.java rename to TTerminalWidget.java index a269609..bf51e6b 100644 --- a/src/jexer/TTerminalWidget.java +++ b/TTerminalWidget.java @@ -28,27 +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 @@ 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 @@ public class TTerminalWidget extends TScrollableWidget */ 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 @@ public class TTerminalWidget extends TScrollableWidget // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Static constructor. + */ + static { + checkForPtypipe(); + } + /** * Public constructor spawns a custom command line. * @@ -198,7 +205,7 @@ public class TTerminalWidget extends TScrollableWidget * @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 @@ public class TTerminalWidget extends TScrollableWidget * @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 @@ public class TTerminalWidget extends TScrollableWidget 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 @@ public class TTerminalWidget extends TScrollableWidget 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 @@ public class TTerminalWidget extends TScrollableWidget * @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 @@ public class TTerminalWidget extends TScrollableWidget * @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 @@ public class TTerminalWidget extends TScrollableWidget // 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 @@ public class TTerminalWidget extends TScrollableWidget ) { 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 @@ public class TTerminalWidget extends TScrollableWidget 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 @@ public class TTerminalWidget extends TScrollableWidget 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 @@ public class TTerminalWidget extends TScrollableWidget // 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 @@ public class TTerminalWidget extends TScrollableWidget } }); } - if (getApplication() != null) { - getApplication().postEvent(new TMenuEvent( - TMenu.MID_REPAINT)); - } + app.doRepaint(); } } @@ -957,6 +1055,19 @@ public class TTerminalWidget extends TScrollableWidget } // 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 @@ public class TTerminalWidget extends TScrollableWidget * 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 @@ public class TTerminalWidget extends TScrollableWidget 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 --git a/src/jexer/TTerminalWidget.properties b/TTerminalWidget.properties similarity index 100% rename from src/jexer/TTerminalWidget.properties rename to TTerminalWidget.properties diff --git a/src/jexer/TTerminalWindow.java b/TTerminalWindow.java similarity index 89% rename from src/jexer/TTerminalWindow.java rename to TTerminalWindow.java index e96c50c..754b7a5 100644 --- a/src/jexer/TTerminalWindow.java +++ b/TTerminalWindow.java @@ -28,35 +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 @@ public class TTerminalWindow extends TScrollableWindow { 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 @@ public class TTerminalWindow extends TScrollableWindow { 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 @@ public class TTerminalWindow extends TScrollableWindow { */ @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 @@ public class TTerminalWindow extends TScrollableWindow { } } + /** + * Get this window's help topic to load. + * + * @return the topic name + */ + @Override + public String getHelpTopic() { + return "Terminal Window"; + } + // ------------------------------------------------------------------------ // TTerminalWindow -------------------------------------------------------- // ------------------------------------------------------------------------ @@ -452,4 +452,29 @@ public class TTerminalWindow extends TScrollableWindow { 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 --git a/src/jexer/TTerminalWindow.properties b/TTerminalWindow.properties similarity index 64% rename from src/jexer/TTerminalWindow.properties rename to TTerminalWindow.properties index ed22f49..44a19f6 100644 --- a/src/jexer/TTerminalWindow.properties +++ b/TTerminalWindow.properties @@ -1,2 +1,4 @@ windowTitle=Terminal statusBarRunning=Terminal session executing... +statusBarHelp=Help +statusBarMenu=Menu diff --git a/src/jexer/TText.java b/TText.java similarity index 97% rename from src/jexer/TText.java rename to TText.java index 22bc4b8..f6d7feb 100644 --- a/src/jexer/TText.java +++ b/TText.java @@ -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 @@ public class TText extends TScrollableWidget { 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 @@ public class TText extends TScrollableWidget { /** * 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 @@ public class TText extends TScrollableWidget { reflowData(); } + /** + * Un-justify the text. + */ + public void unJustify() { + justification = Justification.NONE; + reflowData(); + } + } diff --git a/src/jexer/TTimer.java b/TTimer.java similarity index 100% rename from src/jexer/TTimer.java rename to TTimer.java diff --git a/src/jexer/TVScroller.java b/TVScroller.java similarity index 100% rename from src/jexer/TVScroller.java rename to TVScroller.java diff --git a/src/jexer/TWidget.java b/TWidget.java similarity index 98% rename from src/jexer/TWidget.java rename to TWidget.java index f11cbf3..ce22330 100644 --- a/src/jexer/TWidget.java +++ b/TWidget.java @@ -36,6 +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; @@ -591,9 +592,8 @@ public abstract class TWidget implements Comparable { * @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 +1126,18 @@ public abstract class TWidget implements Comparable { 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: *
    @@ -1139,7 +1151,7 @@ public abstract class TWidget implements Comparable { * 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) ) { @@ -1362,7 +1374,7 @@ public abstract class TWidget implements Comparable { * * @param child TWidget to add */ - private void addChild(final TWidget child) { + public void addChild(final TWidget child) { children.add(child); if ((child.enabled) @@ -1403,6 +1415,19 @@ public abstract class TWidget implements Comparable { * @return TRUE if the child was removed, FALSE if it was not found */ public boolean removeChild(final TWidget child) { +<<<<<<< HEAD:TWidget.java + if (children.remove(child)) { + child.close(); + child.parent = null; + child.window = null; + + resetTabOrder(); + + return true; + } + + return false; +======= if (children.remove(child)) { child.close(); child.parent = null; @@ -1414,6 +1439,7 @@ public abstract class TWidget implements Comparable { } return false; +>>>>>>> upstream-sep2019-tcombo:src/jexer/TWidget.java } /** @@ -1439,9 +1465,9 @@ public abstract class TWidget implements Comparable { if (activeChild != null) { activeChild.active = false; } - child.active = true; - activeChild = child; } + child.active = true; + activeChild = child; } } @@ -2168,6 +2194,21 @@ public abstract class TWidget implements Comparable { 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 --git a/src/jexer/TWindow.java b/TWindow.java similarity index 98% rename from src/jexer/TWindow.java rename to TWindow.java index 58195c9..4d14d0e 100644 --- a/src/jexer/TWindow.java +++ b/TWindow.java @@ -199,6 +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 @@ public class TWindow extends TWidget { } 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 @@ public class TWindow extends TWidget { // 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 @@ public class TWindow extends TWidget { 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 @@ public class TWindow extends TWidget { */ @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 --git a/src/jexer/backend/Backend.java b/backend/Backend.java similarity index 100% rename from src/jexer/backend/Backend.java rename to backend/Backend.java diff --git a/src/jexer/backend/ECMA48Backend.java b/backend/ECMA48Backend.java similarity index 100% rename from src/jexer/backend/ECMA48Backend.java rename to backend/ECMA48Backend.java diff --git a/src/jexer/backend/ECMA48Terminal.java b/backend/ECMA48Terminal.java similarity index 92% rename from src/jexer/backend/ECMA48Terminal.java rename to backend/ECMA48Terminal.java index e2997d2..429e698 100644 --- a/src/jexer/backend/ECMA48Terminal.java +++ b/backend/ECMA48Terminal.java @@ -28,6 +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 @@ 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 @@ public class ECMA48Terminal extends LogicalScreen MOUSE_SGR, } + /** + * Available Jexer images support. + */ + private enum JexerImageOption { + DISABLED, + JPG, + PNG, + RGB, + } + // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -189,6 +202,11 @@ public class ECMA48Terminal extends LogicalScreen */ 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen // 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 @@ public class ECMA48Terminal extends LogicalScreen // 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 @@ public class ECMA48Terminal extends LogicalScreen // 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen * @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 @@ public class ECMA48Terminal extends LogicalScreen * @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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen // 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen * - 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen // 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen } } + 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen 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 @@ public class ECMA48Terminal extends LogicalScreen // System.err.println("CACHE MISS"); } - int imageWidth = cells.get(0).getImage().getWidth(); - int imageHeight = cells.get(0).getImage().getHeight(); + BufferedImage image = cellsToImage(cells); + int fullHeight = image.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); + 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()); - */ + sb.append("\033]444;1;0;"); + sb.append(StringUtils.toBase64(pngOutputStream.toByteArray())); + sb.append("\007"); - 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++) { + } else if (jexerImageOption == JexerImageOption.JPG) { + + // Encode as JPG + ByteArrayOutputStream jpgOutputStream = new ByteArrayOutputStream(1024); + + // 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()); - 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); + 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 ""; } - } - image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth, - imageHeight, rgbArray, 0, totalWidth); - if (totalWidth < getTextWidth()) { - int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB(); + sb.append("\033]444;2;0;"); + sb.append(StringUtils.toBase64(jpgOutputStream.toByteArray())); + sb.append("\007"); - for (int imageX = image.getWidth() - totalWidth; - imageX < image.getWidth(); imageX++) { + } else if (jexerImageOption == JexerImageOption.RGB) { - for (int imageY = 0; imageY < fullHeight; imageY++) { - image.setRGB(imageX, imageY, backgroundColor); - } - } - } + // RGB + sb.append(String.format("\033]444;0;%d;%d;0;", image.getWidth(), + Math.min(image.getHeight(), fullHeight))); - sb.append(String.format("\033]444;%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 @@ public class ECMA48Terminal extends LogicalScreen * @return true if this terminal is emitting Jexer images */ public boolean hasJexerImages() { - return jexerImages; + return (jexerImageOption != JexerImageOption.DISABLED); } // ------------------------------------------------------------------------ diff --git a/src/jexer/backend/GenericBackend.java b/backend/GenericBackend.java similarity index 100% rename from src/jexer/backend/GenericBackend.java rename to backend/GenericBackend.java diff --git a/src/jexer/backend/GlyphMaker.java b/backend/GlyphMaker.java similarity index 98% rename from src/jexer/backend/GlyphMaker.java rename to backend/GlyphMaker.java index 0da2918..e5fcc52 100644 --- a/src/jexer/backend/GlyphMaker.java +++ b/backend/GlyphMaker.java @@ -29,6 +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 @@ 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 @@ class GlyphMakerFont { 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 --git a/backend/HeadlessBackend.java b/backend/HeadlessBackend.java new file mode 100644 index 0000000..19fb589 --- /dev/null +++ b/backend/HeadlessBackend.java @@ -0,0 +1,134 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.backend; + +import java.util.List; + +import jexer.event.TInputEvent; + +/** + * HeadlessBackend + */ +public class HeadlessBackend extends LogicalScreen implements Backend { + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The session information. + */ + private SessionInfo sessionInfo; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + */ + public HeadlessBackend() { + sessionInfo = new TSessionInfo(width, height); + } + + // ------------------------------------------------------------------------ + // Backend ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Getter for sessionInfo. + * + * @return the SessionInfo + */ + public final SessionInfo getSessionInfo() { + return sessionInfo; + } + + /** + * Get a Screen, which displays the text cells to the user. + * + * @return the Screen + */ + public Screen getScreen() { + return this; + } + + /** + * Subclasses must provide an implementation that syncs the logical + * screen to the physical device. + */ + public void flushScreen() { + // NOP + } + + /** + * Check if there are events in the queue. + * + * @return if true, getEvents() has something to return to the application + */ + public boolean hasEvents() { + return false; + } + + /** + * Subclasses must provide an implementation to get keyboard, mouse, and + * screen resize events. + * + * @param queue list to append new events to + */ + public void getEvents(List queue) { + // NOP + } + + /** + * Subclasses must provide an implementation that closes sockets, + * restores console, etc. + */ + public void shutdown() { + // NOP + } + + /** + * Set listener to a different Object. + * + * @param listener the new listening object that run() wakes up on new + * input + */ + public void setListener(final Object listener) { + // NOP + } + + /** + * Reload backend options from System properties. + */ + public void reloadOptions() { + // NOP + } + +} diff --git a/src/jexer/backend/LogicalScreen.java b/backend/LogicalScreen.java similarity index 83% rename from src/jexer/backend/LogicalScreen.java rename to backend/LogicalScreen.java index 4e4aecc..22b7e95 100644 --- a/src/jexer/backend/LogicalScreen.java +++ b/backend/LogicalScreen.java @@ -33,6 +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 @@ public class LogicalScreen implements Screen { 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 --git a/src/jexer/backend/MultiBackend.java b/backend/MultiBackend.java similarity index 100% rename from src/jexer/backend/MultiBackend.java rename to backend/MultiBackend.java diff --git a/src/jexer/backend/MultiScreen.java b/backend/MultiScreen.java similarity index 84% rename from src/jexer/backend/MultiScreen.java rename to backend/MultiScreen.java index 9d66b69..45741c0 100644 --- a/src/jexer/backend/MultiScreen.java +++ b/backend/MultiScreen.java @@ -33,6 +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 @@ public class MultiScreen implements Screen { * @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 @@ public class MultiScreen implements Screen { * @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 @@ public class MultiScreen implements Screen { * @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 @@ public class MultiScreen implements Screen { * @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 @@ public class MultiScreen implements Screen { * @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 @@ public class MultiScreen implements Screen { * @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 @@ public class MultiScreen implements Screen { */ 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 @@ public class MultiScreen implements Screen { */ 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 @@ public class MultiScreen implements Screen { * @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 @@ public class MultiScreen implements Screen { * @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 @@ public class MultiScreen implements Screen { * @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 @@ public class MultiScreen implements Screen { 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 --git a/src/jexer/backend/Screen.java b/backend/Screen.java similarity index 87% rename from src/jexer/backend/Screen.java rename to backend/Screen.java index 2a71073..a9a2053 100644 --- a/src/jexer/backend/Screen.java +++ b/backend/Screen.java @@ -30,6 +30,7 @@ package jexer.backend; import jexer.bits.Cell; import jexer.bits.CellAttributes; +import jexer.bits.Clipboard; /** * Drawing operations API. @@ -409,4 +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 --git a/src/jexer/backend/SessionInfo.java b/backend/SessionInfo.java similarity index 100% rename from src/jexer/backend/SessionInfo.java rename to backend/SessionInfo.java diff --git a/src/jexer/backend/SwingBackend.java b/backend/SwingBackend.java similarity index 100% rename from src/jexer/backend/SwingBackend.java rename to backend/SwingBackend.java diff --git a/src/jexer/backend/SwingComponent.java b/backend/SwingComponent.java similarity index 96% rename from src/jexer/backend/SwingComponent.java rename to backend/SwingComponent.java index 3d1074c..df36333 100644 --- a/src/jexer/backend/SwingComponent.java +++ b/backend/SwingComponent.java @@ -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 @@ class SwingComponent { */ 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 @@ class SwingComponent { */ public SwingComponent(final JComponent component) { this.component = component; + adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER); setupComponent(); } diff --git a/src/jexer/backend/SwingSessionInfo.java b/backend/SwingSessionInfo.java similarity index 100% rename from src/jexer/backend/SwingSessionInfo.java rename to backend/SwingSessionInfo.java diff --git a/src/jexer/backend/SwingTerminal.java b/backend/SwingTerminal.java similarity index 94% rename from src/jexer/backend/SwingTerminal.java rename to backend/SwingTerminal.java index f0ba355..0727efc 100644 --- a/src/jexer/backend/SwingTerminal.java +++ b/backend/SwingTerminal.java @@ -36,6 +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 @@ public class SwingTerminal extends LogicalScreen ) { 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 @@ public class SwingTerminal extends LogicalScreen // 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 @@ public class SwingTerminal extends LogicalScreen 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 @@ public class SwingTerminal extends LogicalScreen } 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 @@ public class SwingTerminal extends LogicalScreen 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 @@ public class SwingTerminal extends LogicalScreen 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 @@ public class SwingTerminal extends LogicalScreen 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 @@ public class SwingTerminal extends LogicalScreen 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 @@ public class SwingTerminal extends LogicalScreen 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 @@ public class SwingTerminal extends LogicalScreen 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 @@ public class SwingTerminal extends LogicalScreen 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 @@ public class SwingTerminal extends LogicalScreen 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 @@ public class SwingTerminal extends LogicalScreen 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 @@ public class SwingTerminal extends LogicalScreen 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 @@ public class SwingTerminal extends LogicalScreen 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 @@ public class SwingTerminal extends LogicalScreen 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 @@ public class SwingTerminal extends LogicalScreen } 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 --git a/src/jexer/backend/TSessionInfo.java b/backend/TSessionInfo.java similarity index 100% rename from src/jexer/backend/TSessionInfo.java rename to backend/TSessionInfo.java diff --git a/src/jexer/backend/TTYSessionInfo.java b/backend/TTYSessionInfo.java similarity index 100% rename from src/jexer/backend/TTYSessionInfo.java rename to backend/TTYSessionInfo.java diff --git a/src/jexer/backend/TWindowBackend.java b/backend/TWindowBackend.java similarity index 100% rename from src/jexer/backend/TWindowBackend.java rename to backend/TWindowBackend.java diff --git a/src/jexer/backend/TerminalReader.java b/backend/TerminalReader.java similarity index 100% rename from src/jexer/backend/TerminalReader.java rename to backend/TerminalReader.java diff --git a/src/jexer/backend/package-info.java b/backend/package-info.java similarity index 100% rename from src/jexer/backend/package-info.java rename to backend/package-info.java diff --git a/src/jexer/bits/Cell.java b/bits/Cell.java similarity index 99% rename from src/jexer/bits/Cell.java rename to bits/Cell.java index a8efa2b..ed3c202 100644 --- a/src/jexer/bits/Cell.java +++ b/bits/Cell.java @@ -419,7 +419,7 @@ public final class Cell extends CellAttributes { 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 --git a/src/jexer/bits/CellAttributes.java b/bits/CellAttributes.java similarity index 99% rename from src/jexer/bits/CellAttributes.java rename to bits/CellAttributes.java index 99366fd..ad86198 100644 --- a/src/jexer/bits/CellAttributes.java +++ b/bits/CellAttributes.java @@ -62,7 +62,6 @@ public class CellAttributes { */ private static final int PROTECT = 0x10; - // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ diff --git a/bits/Clipboard.java b/bits/Clipboard.java new file mode 100644 index 0000000..5c1ea9a --- /dev/null +++ b/bits/Clipboard.java @@ -0,0 +1,276 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.bits; + +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.image.BufferedImage; +import java.io.IOException; + +/** + * Clipboard provides convenience methods to copy text and images to and from + * a shared clipboard. When the system clipboard is available it is used. + */ +public class Clipboard { + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The image last copied to the clipboard. + */ + private BufferedImage image = null; + + /** + * The text string last copied to the clipboard. + */ + private String text = null; + + /** + * The system clipboard, or null if it is not available. + */ + private java.awt.datatransfer.Clipboard systemClipboard = null; + + /** + * The image selection class. + */ + private ImageSelection imageSelection; + + /** + * ImageSelection is used to hold an image while on the clipboard. + */ + private class ImageSelection implements Transferable { + + /** + * Returns an array of DataFlavor objects indicating the flavors the + * data can be provided in. The array should be ordered according to + * preference for providing the data (from most richly descriptive to + * least descriptive). + * + * @return an array of data flavors in which this data can be + * transferred + */ + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[] { DataFlavor.imageFlavor }; + } + + /** + * Returns whether or not the specified data flavor is supported for + * this object. + * + * @param flavor the requested flavor for the data + * @return boolean indicating whether or not the data flavor is + * supported + */ + public boolean isDataFlavorSupported(DataFlavor flavor) { + return DataFlavor.imageFlavor.equals(flavor); + } + + /** + * Returns an object which represents the data to be transferred. The + * class of the object returned is defined by the representation + * class of the flavor. + * + * @param flavor the requested flavor for the data + * @throws IOException if the data is no longer available in the + * requested flavor. + * @throws UnsupportedFlavorException if the requested data flavor is + * not supported. + */ + public Object getTransferData(DataFlavor flavor) + throws UnsupportedFlavorException, IOException { + + if (!DataFlavor.imageFlavor.equals(flavor)) { + throw new UnsupportedFlavorException(flavor); + } + return image; + } + } + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + */ + public Clipboard() { + try { + systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + } catch (java.awt.HeadlessException e) { + // SQUASH + } + } + + // ------------------------------------------------------------------------ + // Clipboard -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Copy an image to the clipboard. + * + * @param image image to copy + */ + public void copyImage(final BufferedImage image) { + this.image = image; + if (systemClipboard != null) { + ImageSelection imageSelection = new ImageSelection(); + systemClipboard.setContents(imageSelection, null); + } + } + + /** + * Copy a text string to the clipboard. + * + * @param text string to copy + */ + public void copyText(final String text) { + this.text = text; + if (systemClipboard != null) { + StringSelection stringSelection = new StringSelection(text); + systemClipboard.setContents(stringSelection, null); + } + } + + /** + * Obtain an image from the clipboard. + * + * @return image from the clipboard, or null if no image is available + */ + public BufferedImage pasteImage() { + if (systemClipboard != null) { + getClipboardImage(); + } + return image; + } + + /** + * Obtain a text string from the clipboard. + * + * @return text string from the clipboard, or null if no text is + * available + */ + public String pasteText() { + if (systemClipboard != null) { + getClipboardText(); + } + return text; + } + + /** + * Returns true if the clipboard has an image. + * + * @return true if an image is available from the clipboard + */ + public boolean isImage() { + if (image == null) { + getClipboardImage(); + } + return (image != null); + } + + /** + * Returns true if the clipboard has a text string. + * + * @return true if a text string is available from the clipboard + */ + public boolean isText() { + if (text == null) { + getClipboardText(); + } + return (text != null); + } + + /** + * Returns true if the clipboard is empty. + * + * @return true if the clipboard is empty + */ + public boolean isEmpty() { + return ((isText() == false) && (isImage() == false)); + } + + /** + * Copy image from the clipboard to text. + */ + private void getClipboardImage() { + if (systemClipboard != null) { + Transferable contents = systemClipboard.getContents(null); + if (contents != null) { + if (contents.isDataFlavorSupported(DataFlavor.imageFlavor)) { + try { + Image img = (Image) contents.getTransferData(DataFlavor.imageFlavor); + image = new BufferedImage(img.getWidth(null), + img.getHeight(null), BufferedImage.TYPE_INT_ARGB); + image.getGraphics().drawImage(img, 0, 0, null); + } catch (IOException e) { + // SQUASH + } catch (UnsupportedFlavorException e) { + // SQUASH + } + } + } + } + } + + /** + * Copy text string from the clipboard to text. + */ + private void getClipboardText() { + if (systemClipboard != null) { + Transferable contents = systemClipboard.getContents(null); + if (contents != null) { + if (contents.isDataFlavorSupported(DataFlavor.stringFlavor)) { + try { + text = (String) contents.getTransferData(DataFlavor.stringFlavor); + } catch (IOException e) { + // SQUASH + } catch (UnsupportedFlavorException e) { + // SQUASH + } + } + } + } + } + + /** + * Clear whatever is on the local clipboard. Note that this will not + * clear the system clipboard. + */ + public void clear() { + image = null; + text = null; + } + +} diff --git a/src/jexer/bits/Color.java b/bits/Color.java similarity index 100% rename from src/jexer/bits/Color.java rename to bits/Color.java diff --git a/src/jexer/bits/ColorTheme.java b/bits/ColorTheme.java similarity index 92% rename from src/jexer/bits/ColorTheme.java rename to bits/ColorTheme.java index ffba4d4..3efce63 100644 --- a/src/jexer/bits/ColorTheme.java +++ b/bits/ColorTheme.java @@ -178,8 +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 @@ public class ColorTheme { // 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 @@ public class ColorTheme { 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 @@ public class ColorTheme { 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 --git a/src/jexer/bits/GraphicsChars.java b/bits/GraphicsChars.java similarity index 100% rename from src/jexer/bits/GraphicsChars.java rename to bits/GraphicsChars.java diff --git a/src/jexer/bits/MnemonicString.java b/bits/MnemonicString.java similarity index 100% rename from src/jexer/bits/MnemonicString.java rename to bits/MnemonicString.java diff --git a/src/jexer/bits/StringUtils.java b/bits/StringUtils.java similarity index 61% rename from src/jexer/bits/StringUtils.java rename to bits/StringUtils.java index fffce20..d33f71f 100644 --- a/src/jexer/bits/StringUtils.java +++ b/bits/StringUtils.java @@ -30,6 +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 @@ import java.util.ArrayList; * * - 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 @@ public class StringUtils { * @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 @@ public class StringUtils { 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 --git a/src/jexer/bits/package-info.java b/bits/package-info.java similarity index 100% rename from src/jexer/bits/package-info.java rename to bits/package-info.java diff --git a/build.xml b/build.xml deleted file mode 100644 index 92d488b..0000000 --- a/build.xml +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Jexer - Java Text User Interface Library]]> - - - Copyright © 2019 Kevin Lamonte. Licensed MIT.]]> - - - - - - diff --git a/src/jexer/demos/Demo1.java b/demos/Demo1.java similarity index 100% rename from src/jexer/demos/Demo1.java rename to demos/Demo1.java diff --git a/src/jexer/demos/Demo2.java b/demos/Demo2.java similarity index 100% rename from src/jexer/demos/Demo2.java rename to demos/Demo2.java diff --git a/src/jexer/demos/Demo2.properties b/demos/Demo2.properties similarity index 100% rename from src/jexer/demos/Demo2.properties rename to demos/Demo2.properties diff --git a/src/jexer/demos/Demo3.java b/demos/Demo3.java similarity index 100% rename from src/jexer/demos/Demo3.java rename to demos/Demo3.java diff --git a/src/jexer/demos/Demo4.java b/demos/Demo4.java similarity index 100% rename from src/jexer/demos/Demo4.java rename to demos/Demo4.java diff --git a/src/jexer/demos/Demo5.java b/demos/Demo5.java similarity index 100% rename from src/jexer/demos/Demo5.java rename to demos/Demo5.java diff --git a/src/jexer/demos/Demo5.properties b/demos/Demo5.properties similarity index 100% rename from src/jexer/demos/Demo5.properties rename to demos/Demo5.properties diff --git a/src/jexer/demos/Demo6.java b/demos/Demo6.java similarity index 99% rename from src/jexer/demos/Demo6.java rename to demos/Demo6.java index db0b5c9..41d1f2c 100644 --- a/src/jexer/demos/Demo6.java +++ b/demos/Demo6.java @@ -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 --git a/src/jexer/demos/Demo6.properties b/demos/Demo6.properties similarity index 100% rename from src/jexer/demos/Demo6.properties rename to demos/Demo6.properties diff --git a/src/jexer/demos/Demo7.java b/demos/Demo7.java similarity index 100% rename from src/jexer/demos/Demo7.java rename to demos/Demo7.java diff --git a/src/jexer/demos/Demo7.properties b/demos/Demo7.properties similarity index 100% rename from src/jexer/demos/Demo7.properties rename to demos/Demo7.properties diff --git a/demos/Demo8.java b/demos/Demo8.java new file mode 100644 index 0000000..19fe5ff --- /dev/null +++ b/demos/Demo8.java @@ -0,0 +1,154 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.demos; + +import java.net.ServerSocket; +import java.net.Socket; +import java.text.MessageFormat; +import java.util.ResourceBundle; + +import jexer.TApplication; +import jexer.backend.*; +import jexer.demos.DemoApplication; +import jexer.net.TelnetServerSocket; + + +/** + * This class shows off the use of MultiBackend and MultiScreen. + */ +public class Demo8 { + + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(Demo8.class.getName()); + + // ------------------------------------------------------------------------ + // Demo8 ------------------------------------------------------------------ + // ------------------------------------------------------------------------ + + /** + * Main entry point. + * + * @param args Command line arguments + */ + public static void main(final String [] args) { + ServerSocket server = null; + try { + + /* + * In this demo we will create a headless application that anyone + * can telnet to. + */ + + /* + * Check the arguments for the port to listen on. + */ + if (args.length == 0) { + System.err.println(i18n.getString("usageString")); + return; + } + int port = Integer.parseInt(args[0]); + + /* + * We create a headless screen and use it to establish a + * MultiBackend. + */ + HeadlessBackend headlessBackend = new HeadlessBackend(); + MultiBackend multiBackend = new MultiBackend(headlessBackend); + + /* + * Now we create the shared application (a standard demo) and + * spin it up. + */ + DemoApplication demoApp = new DemoApplication(multiBackend); + (new Thread(demoApp)).start(); + multiBackend.setListener(demoApp); + + /* + * Fire up the telnet server. + */ + server = new TelnetServerSocket(port); + while (demoApp.isRunning()) { + Socket socket = server.accept(); + System.out.println(MessageFormat. + format(i18n.getString("newConnection"), socket)); + + ECMA48Backend ecmaBackend = new ECMA48Backend(demoApp, + socket.getInputStream(), + socket.getOutputStream()); + + /* + * Add this screen to the MultiBackend, and at this point we + * have the telnet client able to use the shared demo + * application. + */ + multiBackend.addBackend(ecmaBackend); + + /* + * Emit the connection information from telnet. + */ + Thread.sleep(500); + System.out.println(MessageFormat. + format(i18n.getString("terminal"), + ((jexer.net.TelnetInputStream) socket.getInputStream()). + getTerminalType())); + System.out.println(MessageFormat. + format(i18n.getString("username"), + ((jexer.net.TelnetInputStream) socket.getInputStream()). + getUsername())); + System.out.println(MessageFormat. + format(i18n.getString("language"), + ((jexer.net.TelnetInputStream) socket.getInputStream()). + getLanguage())); + + } // while (demoApp.isRunning()) + + /* + * When the application exits, kill all of the connections too. + */ + multiBackend.shutdown(); + server.close(); + + System.out.println(i18n.getString("exitMain")); + + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (server != null) { + try { + server.close(); + } catch (Exception e) { + // SQUASH + } + } + } + } + +} diff --git a/demos/Demo8.properties b/demos/Demo8.properties new file mode 100644 index 0000000..08a8217 --- /dev/null +++ b/demos/Demo8.properties @@ -0,0 +1,6 @@ +usageString=USAGE: java -cp jexer.jar jexer.demos.Demo8 port +newConnection=New connection: {0} +username=\ \ \ username: {0} +language=\ \ \ language: {0} +terminal=\ \ \ terminal: {0} +exitMain=Main thread is exiting... diff --git a/src/jexer/demos/DemoApplication.java b/demos/DemoApplication.java similarity index 100% rename from src/jexer/demos/DemoApplication.java rename to demos/DemoApplication.java diff --git a/src/jexer/demos/DemoApplication.properties b/demos/DemoApplication.properties similarity index 100% rename from src/jexer/demos/DemoApplication.properties rename to demos/DemoApplication.properties diff --git a/src/jexer/demos/DemoCheckBoxWindow.java b/demos/DemoCheckBoxWindow.java similarity index 98% rename from src/jexer/demos/DemoCheckBoxWindow.java rename to demos/DemoCheckBoxWindow.java index fda7bd7..faf3530 100644 --- a/src/jexer/demos/DemoCheckBoxWindow.java +++ b/demos/DemoCheckBoxWindow.java @@ -103,8 +103,9 @@ public class DemoCheckBoxWindow extends TWindow { 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 --git a/src/jexer/demos/DemoCheckBoxWindow.properties b/demos/DemoCheckBoxWindow.properties similarity index 100% rename from src/jexer/demos/DemoCheckBoxWindow.properties rename to demos/DemoCheckBoxWindow.properties diff --git a/src/jexer/demos/DemoEditorWindow.java b/demos/DemoEditorWindow.java similarity index 100% rename from src/jexer/demos/DemoEditorWindow.java rename to demos/DemoEditorWindow.java diff --git a/src/jexer/demos/DemoEditorWindow.properties b/demos/DemoEditorWindow.properties similarity index 100% rename from src/jexer/demos/DemoEditorWindow.properties rename to demos/DemoEditorWindow.properties diff --git a/src/jexer/demos/DemoMainWindow.java b/demos/DemoMainWindow.java similarity index 100% rename from src/jexer/demos/DemoMainWindow.java rename to demos/DemoMainWindow.java diff --git a/src/jexer/demos/DemoMainWindow.properties b/demos/DemoMainWindow.properties similarity index 100% rename from src/jexer/demos/DemoMainWindow.properties rename to demos/DemoMainWindow.properties diff --git a/src/jexer/demos/DemoMsgBoxWindow.java b/demos/DemoMsgBoxWindow.java similarity index 100% rename from src/jexer/demos/DemoMsgBoxWindow.java rename to demos/DemoMsgBoxWindow.java diff --git a/src/jexer/demos/DemoMsgBoxWindow.properties b/demos/DemoMsgBoxWindow.properties similarity index 100% rename from src/jexer/demos/DemoMsgBoxWindow.properties rename to demos/DemoMsgBoxWindow.properties diff --git a/src/jexer/demos/DemoTableWindow.java b/demos/DemoTableWindow.java similarity index 100% rename from src/jexer/demos/DemoTableWindow.java rename to demos/DemoTableWindow.java diff --git a/src/jexer/demos/DemoTableWindow.properties b/demos/DemoTableWindow.properties similarity index 100% rename from src/jexer/demos/DemoTableWindow.properties rename to demos/DemoTableWindow.properties diff --git a/src/jexer/demos/DemoTextFieldWindow.java b/demos/DemoTextFieldWindow.java similarity index 100% rename from src/jexer/demos/DemoTextFieldWindow.java rename to demos/DemoTextFieldWindow.java diff --git a/src/jexer/demos/DemoTextFieldWindow.properties b/demos/DemoTextFieldWindow.properties similarity index 100% rename from src/jexer/demos/DemoTextFieldWindow.properties rename to demos/DemoTextFieldWindow.properties diff --git a/src/jexer/demos/DemoTextWindow.java b/demos/DemoTextWindow.java similarity index 100% rename from src/jexer/demos/DemoTextWindow.java rename to demos/DemoTextWindow.java diff --git a/src/jexer/demos/DemoTextWindow.properties b/demos/DemoTextWindow.properties similarity index 100% rename from src/jexer/demos/DemoTextWindow.properties rename to demos/DemoTextWindow.properties diff --git a/src/jexer/demos/DemoTreeViewWindow.java b/demos/DemoTreeViewWindow.java similarity index 100% rename from src/jexer/demos/DemoTreeViewWindow.java rename to demos/DemoTreeViewWindow.java diff --git a/src/jexer/demos/DemoTreeViewWindow.properties b/demos/DemoTreeViewWindow.properties similarity index 100% rename from src/jexer/demos/DemoTreeViewWindow.properties rename to demos/DemoTreeViewWindow.properties diff --git a/src/jexer/demos/DesktopDemo.java b/demos/DesktopDemo.java similarity index 100% rename from src/jexer/demos/DesktopDemo.java rename to demos/DesktopDemo.java diff --git a/src/jexer/demos/DesktopDemoApplication.java b/demos/DesktopDemoApplication.java similarity index 100% rename from src/jexer/demos/DesktopDemoApplication.java rename to demos/DesktopDemoApplication.java diff --git a/src/jexer/demos/DesktopDemoApplication.properties b/demos/DesktopDemoApplication.properties similarity index 100% rename from src/jexer/demos/DesktopDemoApplication.properties rename to demos/DesktopDemoApplication.properties diff --git a/src/jexer/demos/package-info.java b/demos/package-info.java similarity index 100% rename from src/jexer/demos/package-info.java rename to demos/package-info.java diff --git a/docs/032_announcement.txt b/docs/032_announcement.txt deleted file mode 100644 index ee2d5fc..0000000 --- a/docs/032_announcement.txt +++ /dev/null @@ -1,91 +0,0 @@ -Jexer 0.3.2 Release -=================== - -I am pleased to announce the release of Jexer 0.3.2. This release -completes nearly every feature I set out to make, and is the last -major milestone before 1.0.0. - -Jexer is not an application itself, but rather an advanced text -windowing system framework to help new applications take full -advantage of the terminal. Its major features are: - - * MIT licensed. - - * Direct support for xterm-like terminals: mouse, keyboard, 24-bit - RGB color, UTF-8, fullwidth characters (CJK and emoji), and sixel - images. - - * A Swing-based GUI window that ships with a good-looking Terminus - font. - - * Sixel image support, for both input in its terminal window and - output to the host terminal. Jexer is (to my knowledge) the first - and only system capable of managing multiple terminal windows - displaying properly overlapping images. - - * Draggable / resizable windows, menu bar, and system-modal dialogs - (message/input boxes and filename picker). - - * A full complement of widgets: button, text field, checkbox, - combobox, list, radio button, scrollbars, data table, calendar - picker, progress bar, text display, and simple text editor. Plus - layout manager support for resizable widgets and windows. - - * A terminal window capable of passing "vttest" (including VT100 - double-width / double-height), and supporting all of Jexer's - features. Jexer can run inside itself, with full keyboard, mouse, - and image support. - - * Extensively documented in the code (Javadoc), a wiki, and ships - with a demonstration application showing off all of its available - widgets. - - -Find out more at the Jexer Sourceforge or GitLab project pages: - - * https://jexer.sourceforge.io/ - - * https://gitlab.com/klamonte/jexer - - -Download --------- - -GitLab: git clone https://gitlab.com/klamonte/jexer.git - -Binary downloads: http://sourceforge.net/project/showfiles.php?group_id=2829121 - -On Maven: - - group: com.gitlab.klamonte - artifact: jexer - version: 0.3.2 - - -Ugh, Java Sucks! ----------------- - -(Thor squint) But does it though? - -More seriously, I initially picked D because it was sexy. But D circa -2013 brought too many headaches for me, so I switched to Java because -I wanted a cross-platform standard library that would be stable over -many years. And Java is OK, it is a solid workhorse that gets the job -done. - -Yet in porting my initial work to Java I stumbled upon an unexpected -benefit: I found ways to accomplish all of what Jexer does _without -calling C directly_. No termios, no ncurses, no forkpty(), and thus -no serious hurdles porting it to anything that can spawn programs and -read their output. On Linux, BSD, or OSX, all you need is 'stty' and -'script' to make things work. (And if you want resizable terminal -windows, add 'ptypipe'.) - -So for those who want something like Jexer but in your own favorite -language, I encourage you to check out the [Porting -Jexer](https://gitlab.com/klamonte/jexer/wikis/porting) page on the -wiki: it has pointers to where the key features are, and a potential -roadmap if you wanted to take part or all of it into your own hands. -I licensed Jexer as MIT, stuck with simple Java 1.6, and thoroughly -documented it in the hope that fans of other languages could more -easily create or enhance their own text user interfaces. diff --git a/docs/images.md b/docs/images.md deleted file mode 100644 index 919f63d..0000000 --- a/docs/images.md +++ /dev/null @@ -1,1252 +0,0 @@ -Terminal Emulator Multimedia Standard - Proposed Design -======================================================= - -Version: 1 - - - -Purpose -------- - -Multiple standards exist to incorporate image data in text-based -terminals and terminal emulators. Few standards have wide adoption -despite frequent user requests for these features and hardware support -for several of the standards. - -A group including developers of several widely-used terminal emulators -has been working on defining the needs and limitations for a standard -that can be implemented in current-gen terminal emulators. The -discussion has been primarily captured here: -https://gitlab.freedesktop.org/terminal-wg/specifications/issues/12 - -This document collects many of the reported desires and practical -constraints of that discussion into a proposed standard that -encompasses three independent new features: - -1. A method to transfer multimedia data for immediate display within - the screen cell grid ("Direct Multimedia"). - -2. A method to transfer multimedia data to a terminal-managed cache, - and later display that data within the screen cell grid ("Cached - Multimedia"). - -3. A method to assign cell data to different layers with options for - both layer and cell transparency ("Layers"). - -A terminal may implement any combination of these features -independently of each other. If all features are supported, then all -of the design goals outlined in this document can be met. - -The same mechanisms that can put raster-based images on the screen are -also readily generalizable to other media types such as vector-based -images and animations. This document is thus a "multimedia" proposal -rather than a "simple images" proposal. - - - -Acknowledgements ----------------- - -This proposal has been informed from the following prior work: - -* DEC VT300 series sixel graphics standard: - https://vt100.net/docs/vt3xx-gp/chapter14.html - -* iTerm2 image protocol: - https://iterm2.com/documentation-images.html - -* Kitty image protocol: - https://sw.kovidgoyal.net/kitty/graphics-protocol.html - -* Jexer Terminal User Interface: - https://gitlab.com/klamonte/jexer - - - -Design Goals - Core -------------------- - -The core ("must-have") design goals are: - -* Be easy to implement in existing terminals and applications: - - - Sacrifice "10%" of potential function to eliminate "90%" of - implementation pain. "Less is more." - - - Be a strict superset of the existing iTerm2 and DEC sixel image - solutions. One should be able to take an existing terminal or - application that emits/consumes iTerm2 or sixel sequences, and - only change the control sequence introducer/termination to achieve - the same effect as a terminal/application that conforms with this - standard. - -* Have no ambiguity. If two terminal or application developers can - read this document and reach different conclusions on what should be - on the screen, then an error exists in this document that must be - corrected. - - - Every feature must be straightforward to validate via automated - unit testing. - - - Every conformant terminal must produce the same output (pixels on - screen) given the same input (terminal font, terminal sequences). - - - Every option must have a defined default value. - - - Erroneous sequences must have defined expected results. - - - Every operation must act atomically: either everything worked - (image is on screen, cursor has moved, terminal state has changed, - etc.) or nothing did. - -* Integrate with existing ECMA-48 / ANSI X3.64 defined sequences: - - - Operations on Tiles/Cells containing text will have the same - effect when applied to Tiles/Cells containing image data. - - - Existing sequences are given new parameters to cover needed - features rather than entirely new sequences introduced. - -* Be straightforward to implement in non-"physical" terminals, - including: - - - Future versions of terminal control libraries such as ncurses and - termbox. - - - Terminal multiplexers that support "headless" terminals (no - physical screen) and "multi-head" terminals (many different - physical screens). - -* Be platform-agnostic, and easy to implement on (at the least): - POSIX, Windows, and web. - - - All features must be available even if the only means of - communication between the application and terminal is control - sequences (e.g. no shared disk, no shared memory, no shared DOM, - etc.). - -* Support graceful fallback: - - - Terminal emulators and physical terminals that do not support this - standard should remain usable with no undefined screen artifacts, - even when the application blindly emits these sequences to those - terminals. - - - This standard must able to be versioned for future enhancements. - - - An application must be able to detect that its terminal supports - this standard, and at what version. - -* Support secure programming practices: - - - Applications must not be able to obtain unauthorized data from - terminal memory, such as: images emitted by other applications - still present in the terminal's scrollback buffer, terminal or - system memory limits. - - - Applications must not be able to compromise the terminal through - denial-of-service such as: excessive memory usage, unterminated - control sequences. Similarly, terminals must not be able to - compromise application through their responses to application - queries. - - - Applications must not be able to manipulate the terminal into - performing an insecure operation such as: reading arbitrary shared - memory regions, reading arbitrary files on disk, deleting - arbitrary files on disk, etc. Similarly, terminals must not be - able to manipulate applications into performing insecure - operations. - - - This standard must be implementable when the terminal has a fixed - maximum memory, such as a kernel-level device driver. - - - -Design Goals - Secondary ------------------------- - -The secondary ("nice-to-have") design goals are listed below. These -might not all be possible, but will kept in mind: - -* Minimal redundant network traffic for on-screen data that is - repeated: either on screen in multiple places, or in the same place - but refreshed multiple times. - -* Asynchronous notification from terminal to application that the - screen has been changed by outside or user action. Examples: font - change, session detach/attach, user changed image preferences. - -* The ability for a multiplexer to "pass-thru" the image drawing - sequence to its "outer" terminal, with some support for limited - clipping. - - - -Out Of Scope ------------- - -The following items are out of scope: - -* Bidirectional output. Applications are expected to generate Tiles - and place them on screen where they need. The cursor response to - image sequences are defined as left-to-right-top-to-bottom, - consistent with ECMA-48 / ANSI X3.64 sequences. An independent BIDI - standard is free to apply whatever solution will work for ECMA-48 / - ANSI X3.64 sequences to the sequences described in this document. - -* Capabilities. This standard defines a limited number of new - terminal reports and responses. These are not intended to be used - as a general-purpose capabilities model. - -* Terminal Cache Management. This standard defines a means for - applications and terminals to communicate around cached multimedia - items, but terminals are free to implement whatever cache management - strategies they deem fit. - -* Reliable Transport. This standard defines a two-way - command/response protocol that may get out of order on unreliable - channels such as 3-wire RS232. Applictions that require reliable - transport on unreliable links may choose to use one of the many - successful standards available for this purpose. - - - -Definitions ------------ - -Terminal - The hardware, or a program that simulates hardware, - comprising a keyboard, screen, and mouse. - -Application - A program that utilizes the terminal for its - input/output with the user. - -Multiplexer - A special case of an application that simulates one or - more "inner" terminals for other applications to use, - and composes these inner terminals into a combined - screen to emit to one or more "outer" terminals that - obtain input/output from the user. Multiplexers are - thus both applications and terminals. - -X - The column coordinate of a cell. This standard is 1-based (like - ECMA-48): the left-most column of the screen is numbered 1. - -Y - The row coordinate of a cell. This standard is 1-based (like - ECMA-48): the top-most row of the screen is numbered 1. - -Z - The layer that text or multimedia is placed on. This proposal - uses a right-hand coordinate system with (X, Y, Z) = (1, 1, 1) - defined as the top-left corner on the default layer; positive Z - projects "away" from the user and "into" or "behind" the screen. - Rendering the Cells on the screen must produce the same result as - painter's algorithm (see "Layers - Rendering" section below). - -Cell - A fixed-width-and-height rectangle on the screen. The cells of - the screen are arranged in a grid of X columns and Y rows. A - Cell has dimensions of cellWidth and cellHeight pixels. Every - Cell has a coordinate of (X, Y) (or (X, Y, Z) when the terminal - supports the layers feature). - -Tile - One or more contiguous Cells with data to be displayed. The - data can be text or image data, but not both. A Tile has width - of 1, 2, or more, and a coordinate of (X, Y, Z) that is the - same as its left-most (first) Cell's (X, Y, Z). In practice, - Tiles are typically one Cell wide for ASCII and Latin language - glyphs, and two Cells wide for "fullwidth" glyphs as used in - Asian langauges, emojis, and symbols. This standard does not - preclude Tiles from encompassing entire grapheme clusters. - Note that ECMA-48 / ANSI X3.64 operations are performed against - Tiles, not Cells: if a 2-Cell-wide Tile is deleted via - backspace, then the cursor will decrement on screen by two - columns. - -Layer - A screen-sized grid of Cells that have the same Z coordinate. - Layers are drawn to the screen in descending Z order. Layers - may have optional additional attributes such as transparency. - Layer support is an orthogonal (independent) option to - multimedia support. It is acceptable for terminals to support - multimedia without layers and vice versa. - - - -All Features - Detection ------------------------- - -Applications can detect support for these features using Primary -Device Attributes (DA) and DECID (ESC Z, or 0x9A). - -Terminals that support this standard will repond with additional -parameter(s): "224" for direct multimedia, "225" for cached -multimedia, and "226" for layers. A recap of the parameters xterm -supports is listed below, with these new feature responses included: - -| VT220 (and higher) Response | Description | -|-----------------------------|--------------------------------------------| -| 1 | 132-columns | -| 2 | Printer | -| 3 | ReGIS graphics | -| 4 | Sixel graphics | -| 6 | Selective erase | -| 8 | User-defined keys | -| 9 | National Replacement Character sets | -| 1 5 | Technical characters | -| 1 6 | Locator port | -| 1 7 | Terminal state interrogation | -| 1 8 | User windows | -| 2 1 | Horizontal scrolling | -| 2 2 | ANSI color, e.g., VT525 | -| 2 8 | Rectangular editing | -| 2 9 | ANSI text locator (i.e., DEC Locator mode) | -| 2 2 4 | Direct Multimedia Version 1 | -| 2 2 5 | Cached Multimedia Version 1 | -| 2 2 6 | Layers | - - - -Direct Multimedia - Summary ---------------------------- - -Non-text data (multimedia) can be sent to the terminal for immediate -display in a rectangular (single-layer) region of text Cells. -Multimedia data is transmitted to the terminal using one of two wire -formats described later in this document. - -Setting a Cell to multimedia is a destructive operation: the Cell's -original text is lost. Multimedia pixels will not overlap rendered -text in the same Cell. To achieve pixels overlaid on text, the layers -feature can be used. - -Setting any part of a multi-Cell Tile to multimedia also "breaks up" -the Tile into a range of single Cells. In other words, multimedia can -only be carried by a Cell, not a Tile. - -The pixels of a multimedia Cell are assigned to the Cell's foreground; -multimedia Cells have no background. If a terminal supports the -layers feature, setting a multimedia Cell's foreground transparency to -true/enabled causes that Cell to not be displayed at all; setting its -background transparency to either true/enabled or false/disabled has -no visible effect. - -The pixels of multimedia Cells can come from two sources: - - 1. The application can generate pixels and send them to the terminal - for display at the current cursor position. - - 2. The application can specify a source for the multimedia and the - terminal will generate the pixels for display at the current - cursor position. - - - -Direct Multimedia - Required Support For Existing Sequences ------------------------------------------------------------ - -A terminal with direct multimedia feature must support the following -defined xterm sequences: - -| Sequence | Description | -|----------------|-----------------------------------------------------| -| CSI 16 t | Responds with CSI 6 ; cellHeight ; cellWidth t | -| CSI 18 t | Responds with CSI 8 ; rows ; columns t | - - - -Direct Multimedia - New Sequences ---------------------------------- - -A terminal with direct multimedia feature must support the following -new sequences: - -| Sequence | Command | Description | -|--------------------------------------|-------------|-------------------------| -| OSC 1 3 3 8 ; s i x e l : {data} BEL | SIXEL | Display sixel at (x, y) | -| OSC 1 3 3 8 ; s i x e l : {data} ST | SIXEL | Display sixel at (x, y) | -| OSC 1 3 3 8 ; F i l e = {args} : {data} BEL | DMDISPLAY | Display media at (x, y) | -| OSC 1 3 3 8 ; F i l e = {args} : {data} ST | DMDISPLAY | Display media at (x, y) | -| CSI ? 3 0 0 0 h | DECSET 3000 | Enable SCRCHANGE notification | -| CSI ? 3 0 0 0 l | DECRST 3000 | Disable SCRCHANGE notification | -| OSC 1 3 3 9 ; Pe ; {args} ST | DMRESP | Terminal response to DMDISPLAY | -| CSI ? 3 0 0 1 h | DECSET 3001 | Enable DMDISPLAY responses | -| CSI ? 3 0 0 1 l | DECRST 3001 | Disable DMDISPLAY responses | - - - -If SCRCHANGE is set/enabled, then the terminal will send the "CSI 6 ; -cellHeight ; cellWidth t" when the font size has changed, and "CSI 8 ; -rows ; columns t" when the number of rows/columns on the screen has -changed. - - - -For the SIXEL command: - -* The {data} is a sixel sequence as described in the VT330/340 - Programmer Reference Manual, Chapter 14, available online at: - http://vt100.net/docs/vt3xx-gp/chapter14.html . The {data} is the - "P1 ; P2 ; P3 ; q s..s" portion of the Device Control String, i.e. a - complete sixel sequence minus the leading DCS and trailing ST. - -* The sixel image is processed as shown below. Note that this - behavior is equivalent to Sixel Scrolling mode enabled. - - - The sixel active position starts at the upper-left corner of the - text cursor position. - - - The screen is scrolled up if the image overflows into the bottom - text row. - - - Pixels that would be drawn to the right of the visible region on - screen are discarded. - - - The cursor's final position is on the same column as the starting - cursor position, and on the row immediately below the image. - - -For the DMDISPLAY command: - -* The {args} is a set of key-value pairs (each pair separated by - semicolon (';')), followed by a colon (':'), followed by a base-64 - encoded string ({data}). - -* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z', - 'a' - 'z'). - -* A value is any printable ASCII string not containing whitespace, - colon, or semicolon ('!' - '9', '<' - '~'). - -* Any alpha-numeric key may be specified. A key that is not supported - by the terminal is ignored without error. - -* The multimedia pixels are processed as shown below. - - - The pixel are drawn starting at the upper-left corner of the text - cursor position. - - - If scroll is specified as 1 (enabled), then: - - a. The screen is scrolled up if the image overflows into the - bottom text row. - - b. The cursor's final position is on the same column as the - starting cursor position, and on the row immediately below the - image. - - - If scroll is omitted or specified as 0 (disabled), then: - - a. The screen is never scrolled. - - b. Pixels that would be drawn below the visible region on screen - are discarded. - - c. The cursor's final position is at the same column and row as - the starting cursor position, i.e. the cursor does not move at - all. - - - Pixels that would be drawn to the right of the visible region on - screen are discarded. - - - -The keys for the key-value pairs that must be supported by the -terminal are listed below: - -| Key | Default Value | Description | -|--------------|---------------|----------------------------------------------| -| type | "image/rgb" | mime-type describing data field | -| url | "" | If set, a location containing the media data | -| width | 1 | Number of Cells or pixels wide to display in | -| height | 1 | Number of Cells or pixels high to display in | -| scale | "none" | Scale/zoom option, see below | -| align | "nw" | Align image to edge option, see below | -| sourceX | 0 | Media source X position to display | -| sourceY | 0 | Media source Y position to display | -| sourceWidth | "auto" | Media width in pixels to display | -| sourceHeight | "auto" | Media height in pixels to display | -| scroll | 1 | If 1, scroll the display if needed | - -A terminal may support additional keys. If a key is specified but not -supported by the terminal, then it is ignored without error. - - - -The "type" value is a mime-type string describing the format of the -base64-encoded binary data. The terminal must support at mimunum these -mime-types: - -| Type String | Description | -|---------------|--------------------------------------------------------------| -| "image/rgb" | Big-endian-encoded 24-bit red, green, blue values | -| "image/rgba" | Big-endian-encoded 32-bit red, green, blue, alpha values | -| "image/png" | PNG file data as described by (reference to PNG format) | - -A terminal may support additional types. An application can detect -terminal support for a format by: enabling terminal responses (DECSET -3001), sending a DMDISPLAY command, and examining the terminal's -response sequence for success or error. - - - -The "url" value is a RFC-XXXX defined Universal Resource Located, -encoded in RFC-XXXX form as a printable ASCII string not containing: -whitespace, colon (':'), semicolon (';'), or equals ('='). - -A terminal is not required to support any URLs. - - - -The "width" and "height" values can take the following forms: - -| Value | Meaning | -|-------------------------------|---------------------------| -| N (a positive integer) | Number of Cells | -| Npx (positive integer + "px") | Number of pixels | -| N% (positive integer + "%") | Percent of screen width or height | -| "auto" | Number of pixels as defined by the multimedia data | - - - -The "scale" value can take the following values: - -| Value | Meaning | -|------------|---------------------------------------------------------------| -| "none" | No scaling along either axis. | -| "scale" | Stretch image, preserving aspect ratio, to maximum size in the target area without cropping | -| "stretch" | Stretch along both axes, distorting aspect ratio, to fill the target area | -| "crop" | Stretch along both axes, preserving aspect ration, to completely fill the target area, cropping pixels that will not fit | - - - -The "align" value can take the following values: - -| Value | Meaning | -|------------|-----------------------------------------------------------------| -| "nw" | Media is placed at the top-left corner (northwest) | -| "n" | Media is placed on the top and centered horizontally (north) | -| "ne" | Media is placed at the top-right corner (northest) | -| "w" | Media is placed on the left and centered vertically (west) | -| "c" | Media is centered in the target area (center) | -| "e" | Media is placed on the right and centered vertically (east) | -| "sw" | Media is placed on the bottom-left corner (southwest) | -| "s" | Media is placed on the bottom and centered horizontally (south) | -| "se" | Media is placed on the bottom-right corner (southeast) | - - - -"sourceX", "sourceY", "sourceWidth", and "sourceHeight" define the -rectangle of pixels from the media that will be displayed on the -screen. The ranges for these values is shown below: - -| Key | Minimum Value | Maximum Value | Default Value | -|--------------|---------------|-------------------------------|---------------| -| sourceX | 0 | Media's full width - 1 | 0 | -| sourceY | 0 | Media's full height - 1 | 0 | -| sourceWidth | 1 | Media's full width - sourceX | "auto" | -| sourceHeight | 1 | Media's full height - sourceY | "auto" | - -If any of these values are specified and outside the range, no image -is displayed, and the cursor does not move. "sourceWidth" and -"sourceHeight" can be "auto", which means use the maximum available -width/height (given sourceX/sourceY) from the media's inherent -dimensions. - - - -Direct Multimedia - Terminal Responses / Error Handling -------------------------------------------------------- - -If DMDISPLAY reponses are enabled, then a terminal will respond to the -DMDISPLAY display with DMRESP. DMRESP responses must be sent in the -same sequential order as the DMDISPLAY commands they are responses to: -the terminal may not re-order responses. - -No provision is made for reliable delivery. On unreliable links -(example: 3-wire RS232), the DMDISPLAY and DMRESP command/response -sequence may get out of order. - - - -The format of DMRESP is: - -* Pe - a non-negative integer error code. - -* The {args} is a set of key-value pairs (each pair separated by - semicolon (';')). - -* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z', - 'a' - 'z'). - -* A value is any printable ASCII string not containing whitespace, - colon, or semicolon ('!' - '9', '<' - '~'). - - - -The Pe error codes are defined as: - -| Value | Meaning | {args} containts | -|-------|------------------------------------|--------------------------| -| 0 | No error occurred, i.e. success | nothing | -| 1 | Unsupported "type" | "type" value that was incorrect | -| 2 | Invalid value - no media displayed | "key" that was incorrect | -| 3 | Unsupported key - media displayed | "key" that unsupported | -| 4 | Insufficient memory | nothing | -| 5 | Other error - no media displayed | nothing | -| 6 | Other - media displayed | nothing | -| 7 | Conflicting keys - no media displayed | nothing | -| 8 | RESERVED FOR FUTURE USE | RESERVED FOR FUTURE USE | - -Additional Pe error codes may be returned; any Pe value except 0, 3, -and 6 must mean that the media was not displayed, and the cursor was -not moved. - -If both "type" and "url" are set, no media is diaplyed, the cursor is -not moved, and the DMRESP error code is 7. - - - -Direct Multimedia - Examples ----------------------------- - - - -Cached Multimedia - Summary ---------------------------- - -Non-text data (multimedia) can be sent to the terminal for later -display in a rectangular (single-layer) region of text Cells. -Multimedia data is transmitted to the terminal using the CMCACHE -command described below, and displayed on screen using the CMDISPLAY -command. A single CMCACHE command can support many CMDISPLAY -commands. - -Upon display, setting a Cell to multimedia is a destructive operation: -the Cell's original text is lost. Multimedia pixels will not overlap -rendered text in the same Cell. To achieve pixels overlaid on text, -the layers feature can be used. - -Setting any part of a multi-Cell Tile to multimedia also "breaks up" -the Tile into a range of single Cells. In other words, multimedia can -only be carried by a Cell, not a Tile. - -The pixels of a multimedia Cell are assigned to the Cell's foreground; -multimedia Cells have no background. If a terminal supports the -layers feature, setting a multimedia Cell's foreground transparency to -true/enabled causes that Cell to not be displayed at all; setting its -background transparency to either true/enabled or false/disabled has -no visible effect. - -The pixels of multimedia Cells can come from two sources: - - 1. The application can generate pixels and send them to the terminal - for display at the current cursor position. - - 2. The application can specify a source for the multimedia and the - terminal will generate the pixels for display at the current - cursor position. - - - - -Cached Multimedia - Cache/Memory Management -------------------------------------------- - -The terminal manages a cache of multimedia data on behalf of one or -more applications. Applications request media be stored in the cache, -and if successful the terminal provides an identification number that -applications must use to request display from the cache to the screen. - -The amount of memory and retention/eviction strategy for the cache is -wholly managed by the terminal, with the following restrictions: - -* The terminal may not remove items from the cache that have any - portion being actively displayed on the primary or alternate - screens. - -* The terminal must respond to every CMCACHE command with a new unique - ID. - -The scrollback buffer is permitted, and recommended, to contain only a -few (or zero) multimedia images. Terminals should consider retaining -only the last 2-5 screens' worth of pixel data in the scrollback -buffer. - - - -Cached Multimedia - Required Support For Existing Sequences ------------------------------------------------------------ - -A terminal with cached multimedia feature must support the following -defined xterm sequences: - -| Sequence | Description | -|----------------|-----------------------------------------------------| -| CSI 16 t | Responds with CSI 6 ; cellHeight ; cellWidth t | -| CSI 18 t | Responds with CSI 8 ; rows ; columns t | - - - -Cached Multimedia - New Sequences ---------------------------------- - -A terminal with cached multimedia feature must support the following new -sequences: - -| Sequence | Command | Description | -|--------------------------------------|-----------|-------------------------| -| CSI ? 3 0 0 0 h | DECSET 3000 | Enable SCRCHANGE notification | -| CSI ? 3 0 0 0 l | DECRST 3000 | Disable SCRCHANGE notification | -| OSC 1 3 4 0 ; F i l e = {args} : {data} BEL | CMCACHE | Display media at (x, y) | -| OSC 1 3 4 1 ; Pi ; {args} ST | CMDISPLAY | Display media at (x, y) | -| OSC 1 3 4 2 ; Pi ; Pe ; {args} ST | CMCRESP | Terminal response to CMCACHE | -| OSC 1 3 4 3 ; Pi ; Pe ; {args} ST | CMDRESP | Terminal response to CMDISPLAY | - - - -If SCRCHANGE is set/enabled, then the terminal will send the "CSI 6 ; -cellHeight ; cellWidth t" when the font size has changed, and "CSI 8 ; -rows ; columns t" when the number of rows/columns on the screen -changes. - - - -Cached Multimedia - CMCACHE ---------------------------- - -For the CMCACHE command: - -* The {args} is a set of key-value pairs (each pair separated by - semicolon (';')), followed by a colon (':'), followed by a base-64 - encoded string ({data}). - -* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z', - 'a' - 'z'). - -* A value is any printable ASCII string not containing whitespace, - colon, or semicolon ('!' - '9', '<' - '~'). - - - -The keys for the key-value pairs that must be supported by the -terminal are listed below: - -| Key | Default Value | Description | -|--------------|---------------|----------------------------------------------| -| type | "image/rgb" | mime-type describing data field | -| url | "" | If set, a location containing the media data | - - - -The "type" value is a mime-type string describing the format of the -base64-encoded binary data. The terminal must support at mimunum these -mime-types: - -| Type String | Description | -|---------------|--------------------------------------------------------------| -| "image/rgb" | Big-endian-encoded 24-bit red, green, blue values | -| "image/rgba" | Big-endian-encoded 32-bit red, green, blue, alpha values | -| "image/png" | PNG file data as described by (reference to PNG format) | - -A terminal may support additional types. An application can detect -terminal support for a format by: sending a CMCACHE command, and -examining the terminal's CMCRESP sequence for success or error. - - - -The "url" value is a RFC-XXXX defined Universal Resource Located, -encoded in RFC-XXXX form as a printable ASCII string not containing: -whitespace, colon (':'), semicolon (';'), or equals ('='). - -A terminal is not required to support any URLs. - - - -Cached Multimedia - CMDISPLAY ------------------------------ - -For the CMDISPLAY command: - -* Pi - a non-negative integer media ID that was returned by a CMCRESP - response to a previous CMCACHE command. - -* The {args} is a set of key-value pairs (each pair separated by - semicolon (';')), followed by a colon (':'), followed by a base-64 - encoded string. - -* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z', - 'a' - 'z'). - -* A value is any printable ASCII string not containing whitespace, - colon, or semicolon ('!' - '9', '<' - '~'). - -* Any alpha-numeric key may be specified. A key that is not supported - by the terminal is ignored without error. - -* The multimedia pixels are processed as shown below. - - - The pixel are drawn starting at the upper-left corner of the text - cursor position. - - - If scroll is specified as 1 (enabled), then: - - a. The screen is scrolled up if the image overflows into the - bottom text row. - - b. The cursor's final position is on the same column as the - starting cursor position, and on the row immediately below the - image. - - - If scroll is omitted or specified as 0 (disabled), then: - - a. The screen is never scrolled. - - b. Pixels that would be drawn below the visible region on screen - are discarded. - - c. The cursor's final position is at the same column and row as - the starting cursor position, i.e. the cursor does not move at - all. - - - Pixels that would be drawn to the right of the visible region on - screen are discarded. - - - -The keys for the key-value pairs that must be supported by the -terminal are listed below: - -| Key | Default Value | Description | -|--------------|---------------|----------------------------------------------| -| width | 1 | Number of Cells or pixels wide to display in | -| height | 1 | Number of Cells or pixels high to display in | -| scale | "none" | Scale/zoom option, see below | -| align | "nw" | Align image to edge option, see below | -| sourceX | 0 | Media source X position to display | -| sourceY | 0 | Media source Y position to display | -| sourceWidth | "auto" | Media width in pixels to display | -| sourceHeight | "auto" | Media height in pixels to display | -| scroll | 1 | If 1, scroll the display if needed | - -A terminal may support additional keys. If a key is specified but not -supported by the terminal, then it is ignored without error. - - - -The "width" and "height" values can take the following forms: - -| Value | Meaning | -|-------------------------------|---------------------------| -| N (a positive integer) | Number of Cells | -| Npx (positive integer + "px") | Number of pixels | -| N% (positive integer + "%") | Percent of screen width or height | -| "auto" | Number of pixels as defined by the multimedia data | - - - -The "scale" value can take the following values: - -| Value | Meaning | -|------------|---------------------------------------------------------------| -| "none" | No scaling along either axis. | -| "scale" | Stretch image, preserving aspect ratio, to maximum size in the target area without cropping | -| "stretch" | Stretch along both axes, distorting aspect ratio, to fill the target area | -| "crop" | Stretch along both axes, preserving aspect ration, to completely fill the target area, cropping pixels that will not fit | - - - -The "align" value can take the following values: - -| Value | Meaning | -|------------|-----------------------------------------------------------------| -| "nw" | Media is placed at the top-left corner (northwest) | -| "n" | Media is placed on the top and centered horizontally (north) | -| "ne" | Media is placed at the top-right corner (northest) | -| "w" | Media is placed on the left and centered vertically (west) | -| "c" | Media is centered in the target area (center) | -| "e" | Media is placed on the right and centered vertically (east) | -| "sw" | Media is placed on the bottom-left corner (southwest) | -| "s" | Media is placed on the bottom and centered horizontally (south) | -| "se" | Media is placed on the bottom-right corner (southeast) | - - - -"sourceX", "sourceY", "sourceWidth", and "sourceHeight" define the -rectangle of pixels from the media that will be displayed on the -screen. The ranges for these values is shown below: - -| Key | Minimum Value | Maximum Value | Default Value | -|--------------|---------------|-------------------------------|---------------| -| sourceX | 0 | Media's full width - 1 | 0 | -| sourceY | 0 | Media's full height - 1 | 0 | -| sourceWidth | 1 | Media's full width - sourceX | "auto" | -| sourceHeight | 1 | Media's full height - sourceY | "auto" | - -If any of these values are specified and outside the range, no image -is displayed, and the cursor does not move. "sourceWidth" and -"sourceHeight" can be "auto", which means use the maximum available -width/height (given sourceX/sourceY) from the media's inherent -dimensions. - - - -Cached Multimedia - Error Handling ----------------------------------- - -A terminal will always respond to the CMCACHE command with CMCRESP, -and to the CMDISPLAY command with CMDRESP. Responses must be sent in -the same sequential order as the CMCACHE/CMDISPLAY commands they are -responses to: the terminal may not re-order responses. - -No provision is made for reliable delivery. On unreliable links -(example: 3-wire RS232), the command/response sequence may get out of -order. - - - -Cached Multimedia - Error Handling - CMCRESP --------------------------------------------- - -The format of CMCRESP is: - -* Pi - a non-negative integer media ID. The terminal will generate a - new ID for every image successfully loaded into the cache. The - application must use this ID for CMDISPLAY commands. - -* Pe - a non-negative integer error code. - -* The {args} is a set of key-value pairs (each pair separated by - semicolon (';')). - -* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z', - 'a' - 'z'). - -* A value is any printable ASCII string not containing whitespace, - colon, or semicolon ('!' - '9', '<' - '~'). - - - -The Pe error codes are defined as: - -| Value | Meaning | {args} containts | -|-------|----------------------------------------|--------------------------| -| 0 | No error occurred, i.e. success | nothing | -| 1 | Unsupported "type" | "type" value that was incorrect | -| 2 | Invalid value - no media stored | "key" that was incorrect | -| 3 | Unsupported key - media stored | "key" that unsupported | -| 4 | Insufficient memory - no media stored | nothing | -| 5 | Other error - no media stored | nothing | -| 6 | Other - media stored | nothing | -| 7 | Conflicting keys - no media stored | nothing | -| 8 | RESERVED FOR FUTURE USE | RESERVED FOR FUTURE USE | - -Additional Pe error codes may be returned; any Pe value except 0, 3, -and 6 must mean that the media was not stored in the cache. - -If both "type" and "url" are set, no media is diaplyed, the cursor is -not moved, and the CMCRESP error code is 7. - - - -Cached Multimedia - Error Handling - CMDRESP --------------------------------------------- - -The format of CMDRESP is: - -* Pi - a non-negative integer media ID. - -* Pe - a non-negative integer error code. - -* The {args} is a set of key-value pairs (each pair separated by - semicolon (';')). - -* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z', - 'a' - 'z'). - -* A value is any printable ASCII string not containing whitespace, - colon, or semicolon ('!' - '9', '<' - '~'). - - - -The Pe error codes are defined as: - -| Value | Meaning | {args} containts | -|-------|----------------------------------------|--------------------------| -| 0 | No error occurred, i.e. success | nothing | -| 1 | RESERVED FOR FUTURE USE | RESERVED FOR FUTURE USE | -| 2 | Invalid value - no media displayed | "key" that was incorrect | -| 3 | Unsupported key - media displayed | "key" that unsupported | -| 4 | Insufficient memory - no media displayed | nothing | -| 5 | Other error - no media displayed | nothing | -| 6 | Other - media displayed | nothing | -| 7 | RESERVED FOR FUTURE USE | RESERVED FOR FUTURE USE | -| 8 | Media was evicted - no media displayed | nothing | - -Additional Pe error codes may be returned; any Pe value except 0, 3, -and 6 must mean that the media was not displayed. - - - -Cached Multimedia - Examples ----------------------------- - - - - -Layers - Summary ----------------- - -Layers introduce the concept of a layer "Z" coordinate to the existing -rows ("Y") by columns ("X") grid. Put another way, the -two-dimensional grid of columns-by-rows becomes a three-dimensional -cube of columns-by-rows-by-layers. For this document, the column, -row, and layer coordinates are referred to as X, Y, and Z. This -cartesian coordinate system is right-handed, with the Z axis pointing -"away" from the user "into" the screen. - -An application treats the Z coordinate exactly as it does X and Y -(rows and columns) coordinates: - - * If it attemps to set Z to a value less than 1, then Z is set to 1. - - * If it attempts to set Z to a value greater than the number of - layers, then Z is set to the number of layers. - -New sequences are provided to set and query Z, Y, X; to set and query -the screen cube size; and control visibility of Cells in-front-of -other Cells. - -Operations that can act on more than one Cell are defined such to act -on all layers simultaneously by default; most of these operations can -also be set to act only on the current layer. - - - -Layers - Number of Layers -------------------------- - -A terminal is required to provide between 1 and a finite number of -layers. - -The number of layers may be different between the primary and -alternate screens. - -An application may request that the terminal allocate additional -layers. The terminal is free to honor or ignore such requests as it -sees fit. - -The scrollback buffer is permitted, and recommended, to contain only a -"flattened" single layer. - - - -Layers - Terminal State ------------------------ - -The terminal maintains a complex state at all times. This state -includes variables such as cursor position, foreground/background -color, attributes to apply to the next displayed character, and so on. -The layers feature adds more variables to the state, and these -variables are required to be stored with DECSC (ESC 7) and restored -with DECRC (ESC 8). The new variables are listed below: - -| Mnemonic | Description | Default value | -|----------|-----------------------------|----------------| -| Z | Cursor position Z | 1 | -| MSL | Manipulate single layer | off / disabled | -| TFT | Text foreground transparent | false | -| TBT | Text background transparent | false | - - - -Layers - Required Support For Existing Sequences ------------------------------------------------- - -A terminal with layers feature must support the standard VT100/VT102 -sequences defined in their respective manuals. - - - -Layers - New Sequences ----------------------- - -A terminal with layer feature must support the following new -sequences: - -| Sequence | Command | Description | -|-------------------|-------------|----------------------------------------| -| CSI ? z ; y ; x H | CUPZ | Move cursor to (x, y, z) | -| CSI 2 2 5 ; 1 ; Pa t | SLA | Set layer alpha | -| CSI ? 3 0 0 2 h | DECSET 3002 | Enable Manupulate Single Layer (MSL) | -| CSI ? 3 0 0 2 l | DECRST 3002 | Disable Manupulate Single Layer (MSL) | -| CSI ? l ; h ; w t | RSZCUBE | Resize cube to (layers, height, width) | - -Default parameters and ranges are listed below: - -| Command | Position / Variable | Default Value | Minumum | Maximum | -|---------|---------------------|---------------|---------|-----------| -| CUPZ | 1 / z | 1 | 1 | # layers | -| CUPZ | 2 / y | 1 | 1 | # rows | -| CUPZ | 3 / x | 1 | 1 | # columns | -| SLA | 1 / alpha | 255 | 0 | 255 | -| RSZCUBE | 1 / l | 1 | 1 | varies | -| RSZCUBE | 2 / h | 24 | 1 | varies | -| RSZCUBE | 3 / w | 80 | 1 | varies | - -The terminal must also support the following new queries: - -| Query | Response | Description | -|-----------------|-----------------------|--------------------------------| -| CSI ? 1 0 0 n | CSI ? z ; y ; x n | Report cursor Z, Y, X position | -| CSI ? 1 8 t | CSI ? 8 ; l ; h ; w t | Report the text area cube layers, height, width | - -The terminal must support the following new Set Graphics Rendition -(SGR) character attributes commands: - -| SGR Parameter | Description | -|---------------|---------------------------------------------| -| 2 3 0 | Set text foreground color to transparent | -| 2 3 9 | Set text foreground color to solid (opaque) | -| 2 4 0 | Set text background color to transparent | -| 2 4 9 | Set text background color to solid (opaque) | - - - -Layers - Error Handling ------------------------ - -No additional error reporting is provided for layer feature. - - - -Layers - Rendering ------------------- - -A terminal with layer feature will display its Cells such that the -screen will appear as if it was rendered in the manner of the -pseudo-code below: - -``` -for each layer Z, in descending order from maxZ to minZ: - - for each row Y, in ascending order from minY to maxY: - - for each column X, in ascending order from minX to maxX: - - if tile at (X, Y, Z) background color is solid: - draw rectangle of background color with layer alpha - - if tile at (X, Y, Z) foreground color is solid: - if tile at (X, Y, Z) is glyph: - draw glyph with foreground color with layer alpha - else - draw pixel data of tile as red/green/blue/alpha pixels with - layer alpha - - advance X by tile width - next column - - advance Y by 1 - next row - - decrease Z by 1 -next layer -``` - -A terminal is free to optimize its rendering as it sees fit, so long -as the final screen output looks equivalent to the above method. - - - -Layers - Integration With Existing Sequences --------------------------------------------- - -Sequences that insert characters/lines, delete characters/lines, or -modify larger regions are changed to act upon multiple layers as -defined below. By default, MSL (Manipulate Single Layer) is -off/unset, and Z is 1, so if the application never changes MSL or Z -then these sequences will produce the same visible output as a -terminal without layer support. - -A terminal is not required to support all of these sequences; however, -for those sequences it does support, if it supports the layers feature -then the sequences must behave as shown below: - -| Sequence | Command | Additional behavior | -|------------|-------------|------------------------------------------| -| BS (0x08) | Backspace | Only current layer affected if MSL=on | -| DEL (0x7F) | Delete | Only current layer affected if MSL=on | -| IND (0x84) | Index | Only current layer affected if MSL=on | -| RI (0x8D | Reverse Index | Only current layer affected if MSL=on | -| ESC # 3 | DECDHL | Cells on all layers always affected | -| ESC # 4 | DECDHL | Cells on all layers always affected | -| ESC # 5 | DECSWL | Cells on all layers always affected | -| ESC # 6 | DECDWL | Cells on all layers always affected | -| ESC # 8 | DECALN | All layers > 1 cleared; Z, MSL, TFT, TBT reset to default | -| ESC 7 | DECSC | Also store Z, MSL, TFT, TBT | -| ESC 8 | DECRC | Also restore Z, MSL, TFT, TBT | -| ESC c | RIS | All layers > 1 cleared; Z, MSL, TFT, TBT reset to default | -| CSI @ | ICH | Only current layer affected if MSL=on | -| CSI J | ED | Only current layer affected if MSL=on | -| CSI K | EL | Only current layer affected if MSL=on | -| CSI ? K | DECSEL | Only current layer affected if MSL=on | -| CSI L | IL | Only current layer affected if MSL=on | -| CSI M | DL | Only current layer affected if MSL=on | -| CSI X | ECH | Only current layer affected if MSL=on | -| CSI M | DL | Only current layer affected if MSL=on | -| CSI P | DCH | Only current layer affected if MSL=on | -| CSI R | DECSTBM | Cells on all layers always affected | -| CSI $ t | DECARA | Only current layer affected if MSL=on | -| CSI $ v | DECCRA | Only current layer affected if MSL=on | -| CSI x | DECSACE | Cells on all layers always affected | -| CSI $ x | DECFRA | Only current layer affected if MSL=on | -| CSI $ z | DECERA | Only current layer affected if MSL=on | - -(( TODO: add many more to the above table... )) - -The VT52 sub-mode commands: - -| Sequence | Command | Additional behavior | -|------------|-------------|------------------------------------------| -| ESC J | ED | Only current layer affected if MSL=on | -| ESC K | EL | Only current layer affected if MSL=on | - - - -Layers - Use With Multiplexers ------------------------------- - -Layers are inteded to provide a means for multiplexers to pass on the -job of multimedia support to the "outer" or host terminal. The -proposed mechanics of that is outlined in the pseudo-code below: - -``` -for each inner terminal in descending order from maxZ to minZ: - - emit CUPZ(inner terminal Z, inner terminal Y, inner terminal X) - - draw inner terminal text with standard VT100/VT102/xterm sequences - - for each multimedia sequence emitted by the inner terminal: - emit CUP(inner terminal Y, inner terminal X) - emit multimedia sequences to outer terminal - next multimedia sequence - - decrease Z by 1 -next inner terminal -``` - -The method above may not be effective for complex multi-terminal -screen layouts, but is hoped to work well for many simple cases. - - - -Layers - Examples ------------------ - - - - -References ----------- - -* xterm control sequences: - - -* ECMA-48: diff --git a/docs/images2.md b/docs/images2.md deleted file mode 100644 index 26268cf..0000000 --- a/docs/images2.md +++ /dev/null @@ -1,524 +0,0 @@ -Terminal Emulator Images Standard - Proposed Design - Simplified -================================================================ - -Version: 1 - - - -Purpose -------- - -See the [original proposal](images.md) for purpose, design goals, and -definitions. - -This document is an updated proposal to address feedback on the first -proposal, which included: "overengineered", "hopelessly -overengineered", and "unnecessarily complex." I perceive this -feedback as a positive: it is far easier to imagine a feature and -remove it, than to fail to picture it and need to shoehorn it in -later. - -The original proposal was a superset of every image format referenced, -and generalized beyond to multimedia. This proposal is sharply -reduced from that to: "put this pixel rectangle from the image, into -that cell-based rectangle with specific scaling policy". It is mostly -a subset of the iTerm2 protocol, with: - -* Specifications for what happens to the cursor. - -* More precise definitions of the "preserveAspectRatio" equivalent - options. - -* Explicit restriction to a Cell-based target region. - -* Definition that pixels not covered by image are set to the current - background color. - - - -Tradeoffs ---------- - -Simplifying the original proposal will significantly reduce -complexity, but also eliminates features. The major tradeoffs offered -in this revised proposal are: - -1. Elimination of the layers feature, and with it the ability to place - images behind text. In this proposal, a Cell on the screen will - show either a (part of a) visible image, or a (part of a) text - glyph, but never both. - -2. Elimination of the "url" option, and with it the ability for an - application to specify a filename or other method for the terminal - to find the file data on the local machine. Image data must always - be passed inline with the sequences. - -3. Elimination of response codes, and with it: - - - The ability for multiplexers to blindly pass on the sequences to - their host terminal (because unique IDs are not generated by the - terminal). - - - The ability for applications to reliably detect success or - failure of image display operations. - -4. Elimination of pixel-oriented image placement operations, and with - it the ability of applications to pass on image calculations to the - terminal. An application which requires pixel-perfect rendering - must generate the pixels it needs, aligned such to be displayed at - the top-left corner of the text Cell rectangle. - - - -Summary -------- - -This revised document proposes two independent new features: - -1. A method to transfer image data for immediate display within the - screen Cell grid ("Direct Images"). - -2. A method to transfer image data to a terminal-managed cache, and - later display that data within the screen Cell grid ("Cached - Images"). - -The only difference between the first and second feature is the -presence of an ID key. Direct images do not use an ID key, while -cached images use a store operation with ID key followed by one or -more display operations with ID key. - -Images are applied to text Cells, and once set handled the same way -text Cells are handled: erasing a line erases the image Cells on that -line, inserting a character will shift image Cells on that row over, -scrolling will shift the image up, and so on. Therefore, terminals -will need to be prepared for the scenario that every Cell on the -display is a separate image, with a separate display scaling option -that will need to be re-applied automatically if font metrics change. - - - -All Features - Detection ------------------------- - -Applications can detect support for these features using Primary -Device Attributes (DA) and DECID (ESC Z, or 0x9A). - -Terminals that support this standard will repond with additional -parameter(s): "224" for direct images and "225" for cached images. A -recap of the parameters xterm supports is listed below, with these new -feature responses included: - -| VT220 (and higher) Response | Description | -|-----------------------------|--------------------------------------------| -| 1 | 132-columns | -| 2 | Printer | -| 3 | ReGIS graphics | -| 4 | Sixel graphics | -| 6 | Selective erase | -| 8 | User-defined keys | -| 9 | National Replacement Character sets | -| 1 5 | Technical characters | -| 1 6 | Locator port | -| 1 7 | Terminal state interrogation | -| 1 8 | User windows | -| 2 1 | Horizontal scrolling | -| 2 2 | ANSI color, e.g., VT525 | -| 2 8 | Rectangular editing | -| 2 9 | ANSI text locator (i.e., DEC Locator mode) | -| 2 2 4 | Direct Images Version 1 | -| 2 2 5 | Cached Images Version 1 | - - - -Direct Images - Summary ------------------------ - -Non-text data (images) can be sent to the terminal for immediate -display in a rectangular region of text Cells. Image data is -transmitted to the terminal using a wire format described later in -this document. - -Setting a Cell to image is a destructive operation: the Cell's -original text is lost. Similarly, setting a Cell (or multiple Cells -for fullwidth glyphs or grapheme clusters) to text is a destructive -operation: the image in the Cell(s) is lost. - -Setting any part of a multi-Cell Tile to image also "breaks up" the -Tile into a range of single Cells. In other words, image data can -only be carried by a Cell, not a Tile. - - - -Direct Images - New Sequences ------------------------------ - -A terminal with direct images feature must support the following new -sequences: - -| Sequence | Description | -|--------------------------------------|-------------------------| -| OSC 1 3 3 8 ; F i l e = {args} : {data} BEL | Display image at (x, y) | -| OSC 1 3 3 8 ; F i l e = {args} : {data} ST | Display image at (x, y) | - - - -For the OSC 1 3 3 8 sequence: - -* The {args} is a set of key-value pairs (each pair separated by - semicolon (';')), followed by a colon (':'), followed by a base-64 - encoded string ({data}). - -* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z', - 'a' - 'z'). - -* A value is any printable ASCII string not containing whitespace, - colon, or semicolon ('!' - '9', '<' - '~'). - -* Any alpha-numeric key may be specified. A key that is not supported - by the terminal is ignored without error. - -* The image is processed as shown below: - - - The pixels are drawn starting at the upper-left corner of the text - cursor position. - - - All pixels in the target Cell rectangle that are not covered by - the image itself are set the current background color (like - sixel raster attributes). - - - If scroll is specified as 1 (enabled), then: - - a. The screen is scrolled up if the image overflows into the - bottom text row. - - b. The cursor's final position is on the same column as the - starting cursor position, and on the row immediately below the - image. - - - If scroll is omitted or specified as 0 (disabled), then: - - a. The screen is never scrolled. - - b. Pixels that would be drawn below the visible region on screen - are discarded. - - c. The cursor's final position is at the same column and row as - the starting cursor position, i.e. the cursor does not move at - all. - - - Pixels that would be drawn to the right of the visible region on - screen are discarded. - - - If scale is "none", then pixels that would be drawn outside the - target Cell rectangle are discarded. - - - -The keys for the key-value pairs that must be supported by the -terminal are listed below: - -| Key | Default Value | Description | -|--------------|---------------|---------------------------------------| -| type | "image/rgb" | mime-type describing data field | -| width | 1 | Number of Cell columns to display in | -| height | 1 | Number of Cells rows to display in | -| scale | "none" | Scale/zoom option, see below | -| sourceX | 0 | Media source X position to display | -| sourceY | 0 | Media source Y position to display | -| sourceWidth | "auto" | Media width in pixels to display | -| sourceHeight | "auto" | Media height in pixels to display | -| scroll | 0 | If 0, scroll the display if needed | - -A terminal may support additional keys. If a key is specified but not -supported by the terminal, then it is ignored without error. - - - -The "type" value is a mime-type string describing the format of the -base64-encoded binary data. The terminal must support at minimum these -mime-types: - -| Type String | Description | -|---------------|--------------------------------------------------------------| -| "image/rgb" | Big-endian-encoded 24-bit red, green, blue values | -| "image/rgba" | Big-endian-encoded 32-bit red, green, blue, alpha values | -| "image/png" | PNG file data as described by (reference to PNG format) | - -A terminal may support additional types. An application can detect -terminal support for a format by: - - 1. Attempt to draw image, with "scroll" set to 1. - - 2. Check cursor position DSR 6. - - 3. If cursor has moved, then the terminal supports this image type. - - - -The "width" and "height" values are positive integers describing the -number of Cells the image will be placed in. - - - -The "scale" value can take the following values: - -| Value | Meaning | -|------------|---------------------------------------------------------------| -| "none" | No scaling along either axis. | -| "scale" | Stretch image, preserving aspect ratio, to maximum size in the target area without cropping | -| "stretch" | Stretch along both axes, distorting aspect ratio, to fill the target area | -| "crop" | Stretch along both axes, preserving aspect ration, to completely fill the target area, cropping pixels that will not fit | - - - -"sourceX", "sourceY", "sourceWidth", and "sourceHeight" define the -rectangle of pixels from the media that will be displayed on the -screen. The ranges for these values is shown below: - -| Key | Minimum Value | Maximum Value | Default Value | -|--------------|---------------|-------------------------------|---------------| -| sourceX | 0 | Media's full width - 1 | 0 | -| sourceY | 0 | Media's full height - 1 | 0 | -| sourceWidth | 1 | Media's full width - sourceX | "auto" | -| sourceHeight | 1 | Media's full height - sourceY | "auto" | - -If any of these values are specified and outside the range, no image -is displayed, and the cursor does not move. "sourceWidth" and -"sourceHeight" can be "auto", which means use the maximum available -width/height (given sourceX/sourceY) from the media's inherent -dimensions. - - - -Cached Images - Summary ------------------------ - -Non-text data (image) can be sent to the terminal for later display in -a rectangular region of text Cells. Image data is transmitted to the -terminal using the CSTORE command described below, and displayed on -screen using the CDISPLAY command. A single CSTORE command can -support many CDISPLAY commands. - -Upon display, setting a Cell to image is a destructive operation: the -Cell's original text is lost. Similarly, setting a Cell (or multiple -Cells for fullwidth glyphs or grapheme clusters) to text is a -destructive operation: the image in the Cell(s) is lost. - -Setting any part of a multi-Cell Tile to image also "breaks up" the -Tile into a range of single Cells. In other words, image data can -only be carried by a Cell, not a Tile. - - - -Cached Images - Cache/Memory Management ---------------------------------------- - -The terminal manages a cache of multimedia data on behalf of the -application. The application requests media be stored in the cache -and provides an ID. This ID is later used to request display on the -screen. - -The amount of memory and retention/eviction strategy for the cache is -wholly managed by the terminal, with the following restrictions: - -* The terminal may not remove items from the cache that have any - portion being actively displayed on the primary or alternate - screens. - -The scrollback buffer is permitted, and recommended, to contain only a -few (or zero) multimedia images. Terminals should consider retaining -only the last 2-5 screens' worth of pixel data in the scrollback -buffer. - -Applications have no control over when images are removed from the -cache, and no provision is made to generate/ensure unique IDs. - -A terminal multiplexer that passes all CSTORE/CDISPLAY commands to the -host terminal will need to parse the CSTORE and CDISPLAY sequences for -the "id" field and rewrite it to be unique for all of its inner -terminals. - - - -Cached Images - New Sequences ------------------------------ - -A terminal with cached images feature must support the following new -sequences: - -| Sequence | Command | Description | -|--------------------------------------|-----------|--------------------------| -| OSC 1 3 4 0 ; F i l e = {args} : {data} BEL | CSTORE | Store image in cache | -| OSC 1 3 4 0 ; F i l e = {args} : {data} ST | CSTORE | Store image in cache | -| OSC 1 3 4 1 ; Pi ; {args} BEL | CDISPLAY | Display image at (x, y) | -| OSC 1 3 4 1 ; Pi ; {args} ST | CDISPLAY | Display image at (x, y) | - - - -Cached Images - CSTORE ----------------------- - -For the CSTORE command: - -* The {args} is a set of key-value pairs (each pair separated by - semicolon (';')), followed by a colon (':'), followed by a base-64 - encoded string ({data}). - -* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z', - 'a' - 'z'). - -* A value is any printable ASCII string not containing whitespace, - colon, or semicolon ('!' - '9', '<' - '~'). - - - -The keys for the key-value pairs that must be supported by the -terminal are listed below: - -| Key | Default Value | Description | -|--------------|---------------|----------------------------------------------| -| id | 0 | ID to refer to the image | -| type | "image/rgb" | mime-type describing data field | - - - -The "id" value is a non-negative integer between 0 and 999999. - - - -The "type" value is a mime-type string describing the format of the -base64-encoded binary data. The terminal must support at mimunum these -mime-types: - -| Type String | Description | -|---------------|--------------------------------------------------------------| -| "image/rgb" | Big-endian-encoded 24-bit red, green, blue values | -| "image/rgba" | Big-endian-encoded 32-bit red, green, blue, alpha values | -| "image/png" | PNG file data as described by (reference to PNG format) | - -A terminal may support additional types. An application can detect -terminal support for a format by: - - 1. Store image in cache. - - 2. Attempt to draw image, with "scroll" set to 1. - - 3. Check cursor position DSR 6. - - 4. If cursor has moved, then the terminal supports this image type. - - - -Cached Images - CDISPLAY ------------------------- - -For the CDISPLAY command: - -* Pi - a non-negative integer ID that was used in a previous CSTORE - command. - -* The {args} is a set of key-value pairs (each pair separated by - semicolon (';')), followed by a colon (':'), followed by a base-64 - encoded string. - -* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z', - 'a' - 'z'). - -* A value is any printable ASCII string not containing whitespace, - colon, or semicolon ('!' - '9', '<' - '~'). - -* Any alpha-numeric key may be specified. A key that is not supported - by the terminal is ignored without error. - -* The image pixels are processed as shown below. - - - The pixel are drawn starting at the upper-left corner of the text - cursor position. - - - If scroll is specified as 1 (enabled), then: - - a. The screen is scrolled up if the image overflows into the - bottom text row. - - b. The cursor's final position is on the same column as the - starting cursor position, and on the row immediately below the - image. - - - If scroll is omitted or specified as 0 (disabled), then: - - a. The screen is never scrolled. - - b. Pixels that would be drawn below the visible region on screen - are discarded. - - c. The cursor's final position is at the same column and row as - the starting cursor position, i.e. the cursor does not move at - all. - - - Pixels that would be drawn to the right of the visible region on - screen are discarded. - - - -The keys for the key-value pairs that must be supported by the -terminal are listed below: - -| Key | Default Value | Description | -|--------------|---------------|---------------------------------------| -| id | 0 | ID to refer to the image | -| width | 1 | Number of Cell columns to display in | -| height | 1 | Number of Cells rows to display in | -| scale | "none" | Scale/zoom option, see below | -| sourceX | 0 | Media source X position to display | -| sourceY | 0 | Media source Y position to display | -| sourceWidth | "auto" | Media width in pixels to display | -| sourceHeight | "auto" | Media height in pixels to display | -| scroll | 0 | If 1, scroll the display if needed | - -A terminal may support additional keys. If a key is specified but not -supported by the terminal, then it is ignored without error. - - - -The "width" and "height" values are positive integers describing the -number of Cells the image will be placed in. - - - -The "scale" value can take the following values: - -| Value | Meaning | -|------------|---------------------------------------------------------------| -| "none" | No scaling along either axis. | -| "scale" | Stretch image, preserving aspect ratio, to maximum size in the target area without cropping | -| "stretch" | Stretch along both axes, distorting aspect ratio, to fill the target area | -| "crop" | Stretch along both axes, preserving aspect ration, to completely fill the target area, cropping pixels that will not fit | - - - -"sourceX", "sourceY", "sourceWidth", and "sourceHeight" define the -rectangle of pixels from the media that will be displayed on the -screen. The ranges for these values is shown below: - -| Key | Minimum Value | Maximum Value | Default Value | -|--------------|---------------|-------------------------------|---------------| -| sourceX | 0 | Media's full width - 1 | 0 | -| sourceY | 0 | Media's full height - 1 | 0 | -| sourceWidth | 1 | Media's full width - sourceX | "auto" | -| sourceHeight | 1 | Media's full height - sourceY | "auto" | - -If any of these values are specified and outside the range, no image -is displayed, and the cursor does not move. "sourceWidth" and -"sourceHeight" can be "auto", which means use the maximum available -width/height (given sourceX/sourceY) from the media's inherent -dimensions. - - - -Miscellaneous Items -------------------- - -"image/rgb" and "image/rgba" also need width/height fields. Propose -to specify them as 16-bit unsigned ints, followed by 24-bit or 32-bit -data. If data is short, then the rest of the image is assumed to be -current background color (like sixel raster attributes). diff --git a/src/jexer/event/TCommandEvent.java b/event/TCommandEvent.java similarity index 100% rename from src/jexer/event/TCommandEvent.java rename to event/TCommandEvent.java diff --git a/src/jexer/event/TInputEvent.java b/event/TInputEvent.java similarity index 100% rename from src/jexer/event/TInputEvent.java rename to event/TInputEvent.java diff --git a/src/jexer/event/TKeypressEvent.java b/event/TKeypressEvent.java similarity index 100% rename from src/jexer/event/TKeypressEvent.java rename to event/TKeypressEvent.java diff --git a/src/jexer/event/TMenuEvent.java b/event/TMenuEvent.java similarity index 100% rename from src/jexer/event/TMenuEvent.java rename to event/TMenuEvent.java diff --git a/src/jexer/event/TMouseEvent.java b/event/TMouseEvent.java similarity index 87% rename from src/jexer/event/TMouseEvent.java rename to event/TMouseEvent.java index 496d8bc..e529898 100644 --- a/src/jexer/event/TMouseEvent.java +++ b/event/TMouseEvent.java @@ -118,6 +118,21 @@ public class TMouseEvent extends TInputEvent { */ 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 @@ public class TMouseEvent extends TInputEvent { * @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 @@ public class TMouseEvent extends TInputEvent { this.mouse3 = mouse3; this.mouseWheelUp = mouseWheelUp; this.mouseWheelDown = mouseWheelDown; + this.alt = alt; + this.ctrl = ctrl; + this.shift = shift; } // ------------------------------------------------------------------------ @@ -289,6 +311,33 @@ public class TMouseEvent extends TInputEvent { 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 @@ public class TMouseEvent extends TInputEvent { */ 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 @@ public class TMouseEvent extends TInputEvent { */ @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 @@ public class TMouseEvent extends TInputEvent { mouse2, mouse3, mouseWheelUp, - mouseWheelDown); + mouseWheelDown, + alt, ctrl, shift); } } diff --git a/src/jexer/event/TResizeEvent.java b/event/TResizeEvent.java similarity index 100% rename from src/jexer/event/TResizeEvent.java rename to event/TResizeEvent.java diff --git a/src/jexer/event/package-info.java b/event/package-info.java similarity index 100% rename from src/jexer/event/package-info.java rename to event/package-info.java diff --git a/examples/HelloWorld.java b/examples/HelloWorld.java deleted file mode 100644 index 1246959..0000000 --- a/examples/HelloWorld.java +++ /dev/null @@ -1,12 +0,0 @@ -import jexer.TApplication; - -public class HelloWorld { - - public static void main(String [] args) throws Exception { - TApplication app = new TApplication(TApplication.BackendType.XTERM); - app.addToolMenu(); - app.addFileMenu(); - app.addWindowMenu(); - app.run(); - } -} diff --git a/examples/JexerImageViewer.java b/examples/JexerImageViewer.java deleted file mode 100644 index 4839c23..0000000 --- a/examples/JexerImageViewer.java +++ /dev/null @@ -1,312 +0,0 @@ -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import javax.imageio.ImageIO; - -import jexer.TAction; -import jexer.TApplication; -import jexer.TDesktop; -import jexer.TDirectoryList; -import jexer.TImage; -import jexer.backend.SwingTerminal; -import jexer.bits.CellAttributes; -import jexer.bits.GraphicsChars; -import jexer.event.TKeypressEvent; -import jexer.event.TResizeEvent; -import jexer.menu.TMenu; -import jexer.ttree.TDirectoryTreeItem; -import jexer.ttree.TTreeItem; -import jexer.ttree.TTreeViewWidget; -import static jexer.TKeypress.*; - -/** - * Implements a simple image thumbnail file viewer. Much of this code was - * stripped down from TFileOpenBox. - */ -public class JexerImageViewer extends TApplication { - - /** - * Main entry point. - */ - public static void main(String [] args) throws Exception { - JexerImageViewer app = new JexerImageViewer(); - (new Thread(app)).start(); - } - - /** - * Public constructor chooses the ECMA-48 / Xterm backend. - */ - public JexerImageViewer() throws Exception { - super(BackendType.XTERM); - - // The stock tool menu has items for redrawing the screen, opening - // images, and (when using the Swing backend) setting the font. - addToolMenu(); - - // We will have one menu containing a mix of new and stock commands - TMenu fileMenu = addMenu("&File"); - - // Stock commands: a new shell, exit program. - fileMenu.addDefaultItem(TMenu.MID_SHELL); - fileMenu.addSeparator(); - fileMenu.addDefaultItem(TMenu.MID_EXIT); - - // Filter the files list to support image suffixes only. - List filters = new ArrayList(); - filters.add("^.*\\.[Jj][Pp][Gg]$"); - filters.add("^.*\\.[Jj][Pp][Ee][Gg]$"); - filters.add("^.*\\.[Pp][Nn][Gg]$"); - filters.add("^.*\\.[Gg][Ii][Ff]$"); - filters.add("^.*\\.[Bb][Mm][Pp]$"); - setDesktop(new ImageViewerDesktop(this, ".", filters)); - } - -} - -/** - * The desktop contains a tree view on the left, list of files on the top - * right, and image view on the bottom right. - */ -class ImageViewerDesktop extends TDesktop { - - /** - * The left-side tree view pane. - */ - private TTreeViewWidget treeView; - - /** - * The data behind treeView. - */ - private TDirectoryTreeItem treeViewRoot; - - /** - * The top-right-side directory list pane. - */ - private TDirectoryList directoryList; - - /** - * The bottom-right-side image pane. - */ - private TImage imageWidget; - - /** - * Public constructor. - * - * @param application the TApplication that manages this window - * @param path path of selected file - * @param filters a list of strings that files must match to be displayed - * @throws IOException of a java.io operation throws - */ - public ImageViewerDesktop(final TApplication application, final String path, - final List filters) throws IOException { - - super(application); - setActive(true); - - // Add directory treeView - treeView = addTreeViewWidget(0, 0, getWidth() / 2, getHeight(), - new TAction() { - public void DO() { - TTreeItem item = treeView.getSelected(); - File selectedDir = ((TDirectoryTreeItem) item).getFile(); - try { - directoryList.setPath(selectedDir.getCanonicalPath()); - if (directoryList.getList().size() > 0) { - setThumbnail(directoryList.getPath()); - } else { - if (imageWidget != null) { - getChildren().remove(imageWidget); - } - imageWidget = null; - } - activate(treeView); - } catch (IOException e) { - // If the backend is Swing, we can emit the stack - // trace to stderr. Otherwise, just squash it. - if (getScreen() instanceof SwingTerminal) { - e.printStackTrace(); - } - } - } - } - ); - treeViewRoot = new TDirectoryTreeItem(treeView, path, true); - - // Add directory files list - directoryList = addDirectoryList(path, getWidth() / 2 + 1, 0, - getWidth() / 2 - 1, getHeight() / 2, - - new TAction() { - public void DO() { - setThumbnail(directoryList.getPath()); - } - }, - new TAction() { - - public void DO() { - setThumbnail(directoryList.getPath()); - } - }, - filters); - - if (directoryList.getList().size() > 0) { - activate(directoryList); - setThumbnail(directoryList.getPath()); - } else { - activate(treeView); - } - } - - /** - * Handle window/screen resize events. - * - * @param event resize event - */ - @Override - public void onResize(final TResizeEvent event) { - - // Resize the tree and list - treeView.setY(1); - treeView.setWidth(getWidth() / 2); - treeView.setHeight(getHeight() - 1); - treeView.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, - treeView.getWidth(), - treeView.getHeight())); - treeView.getTreeView().onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, - treeView.getWidth() - 1, - treeView.getHeight() - 1)); - directoryList.setX(getWidth() / 2 + 1); - directoryList.setY(1); - directoryList.setWidth(getWidth() / 2 - 1); - directoryList.setHeight(getHeight() / 2 - 1); - directoryList.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, - directoryList.getWidth(), - directoryList.getHeight())); - - // Recreate the image - if (imageWidget != null) { - getChildren().remove(imageWidget); - } - imageWidget = null; - if (directoryList.getList().size() > 0) { - activate(directoryList); - setThumbnail(directoryList.getPath()); - } else { - activate(treeView); - } - } - - /** - * Handle keystrokes. - * - * @param keypress keystroke event - */ - @Override - public void onKeypress(final TKeypressEvent keypress) { - - if (treeView.isActive() || directoryList.isActive()) { - if ((keypress.equals(kbEnter)) - || (keypress.equals(kbUp)) - || (keypress.equals(kbDown)) - || (keypress.equals(kbPgUp)) - || (keypress.equals(kbPgDn)) - || (keypress.equals(kbHome)) - || (keypress.equals(kbEnd)) - ) { - // Tree view will be changing, update the directory list. - super.onKeypress(keypress); - - // This is the same action as treeView's enter. - TTreeItem item = treeView.getSelected(); - File selectedDir = ((TDirectoryTreeItem) item).getFile(); - try { - if (treeView.isActive()) { - directoryList.setPath(selectedDir.getCanonicalPath()); - } - if (directoryList.getList().size() > 0) { - activate(directoryList); - setThumbnail(directoryList.getPath()); - } else { - if (imageWidget != null) { - getChildren().remove(imageWidget); - } - imageWidget = null; - activate(treeView); - } - } catch (IOException e) { - // If the backend is Swing, we can emit the stack trace - // to stderr. Otherwise, just squash it. - if (getScreen() instanceof SwingTerminal) { - e.printStackTrace(); - } - } - return; - } - } - - // Pass to my parent - super.onKeypress(keypress); - } - - /** - * Draw me on screen. - */ - @Override - public void draw() { - CellAttributes background = getTheme().getColor("tdesktop.background"); - putAll(' ', background); - - vLineXY(getWidth() / 2, 0, getHeight(), - GraphicsChars.WINDOW_SIDE, getBackground()); - - hLineXY(getWidth() / 2, getHeight() / 2, (getWidth() + 1) / 2, - GraphicsChars.WINDOW_TOP, getBackground()); - - putCharXY(getWidth() / 2, getHeight() / 2, - GraphicsChars.WINDOW_LEFT_TEE, getBackground()); - } - - /** - * Set the image thumbnail. - * - * @param file the image file - */ - private void setThumbnail(final File file) { - if (file == null) { - return; - } - if (!file.exists() || !file.isFile()) { - return; - } - - BufferedImage image = null; - try { - image = ImageIO.read(file); - } catch (IOException e) { - // If the backend is Swing, we can emit the stack trace to - // stderr. Otherwise, just squash it. - if (getScreen() instanceof SwingTerminal) { - e.printStackTrace(); - } - return; - } - - if (imageWidget != null) { - getChildren().remove(imageWidget); - } - int width = getWidth() / 2 - 1; - int height = getHeight() / 2 - 1; - - imageWidget = new TImage(this, getWidth() - width, - getHeight() - height, width, height, image, 0, 0, null); - - // Resize the image to fit within the pane. - imageWidget.setScaleType(TImage.Scale.SCALE); - - imageWidget.setActive(false); - activate(directoryList); - } - -} diff --git a/examples/JexerTilingWindowManager.java b/examples/JexerTilingWindowManager.java deleted file mode 100644 index 5b5740a..0000000 --- a/examples/JexerTilingWindowManager.java +++ /dev/null @@ -1,223 +0,0 @@ -import jexer.TApplication; -import jexer.TTerminalWindow; -import jexer.TWindow; -import jexer.event.TKeypressEvent; -import jexer.event.TMenuEvent; -import jexer.event.TMouseEvent; -import jexer.event.TResizeEvent; -import jexer.menu.TMenu; - -/** - * Implements a simple tiling window manager. A root non-moveable - * non-resizable terminal window is created first, which can be split - * horizontally or vertically. Each new window retains a reference to its - * "parent", and upon closing resizes that parent back to its original size. - * - * This example shows what can be done with minimal changes to stock Jexer - * widgets. You will quickly see that closing a "parent" tile does not cause - * the "child" tile to resize. You could make a real subclass of - * TTerminalWindow that has extra fields and/or communicates more with - * JexerTilingWindowManager to get full coverage of tile creation, - * destruction, placement, movement, and so on. - */ -public class JexerTilingWindowManager extends TApplication { - - /** - * Menu item: split the terminal vertically. - */ - private static final int MENU_SPLIT_VERTICAL = 2000; - - /** - * Menu item: split the terminal horizontally. - */ - private static final int MENU_SPLIT_HORIZONTAL = 2001; - - /** - * Main entry point. - */ - public static void main(String [] args) throws Exception { - // For this application, we must use ptypipe so that the tile shells - // can be aware of their size. - System.setProperty("jexer.TTerminal.ptypipe", "true"); - - JexerTilingWindowManager jtwm = new JexerTilingWindowManager(); - (new Thread(jtwm)).start(); - } - - /** - * Public constructor chooses the ECMA-48 / Xterm backend. - */ - public JexerTilingWindowManager() throws Exception { - super(BackendType.XTERM); - - // The stock tool menu has items for redrawing the screen, opening - // images, and (when using the Swing backend) setting the font. - addToolMenu(); - - // We will have one menu containing a mix of new and stock commands - TMenu tileMenu = addMenu("&Tile"); - - // New commands for this example: split vertical and horizontal. - tileMenu.addItem(MENU_SPLIT_VERTICAL, "&Vertical Split"); - tileMenu.addItem(MENU_SPLIT_HORIZONTAL, "&Horizontal Split"); - - // Stock commands: a new shell with resizable window, previous, next, - // close, and exit program. - tileMenu.addItem(TMenu.MID_SHELL, "&Floating"); - tileMenu.addSeparator(); - tileMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS); - tileMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT); - tileMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE); - tileMenu.addSeparator(); - tileMenu.addDefaultItem(TMenu.MID_EXIT); - - // Spin up the root tile - TTerminalWindow rootTile = makeTile(0, 0, getScreen().getWidth(), - getDesktopBottom() - 1, null); - - // Let's add some bling! Enable focus-follows-mouse. - setFocusFollowsMouse(true); - } - - /** - * Process menu events. - */ - @Override - protected boolean onMenu(TMenuEvent event) { - if (event.getId() == MENU_SPLIT_VERTICAL) { - splitVertical(); - return true; - } - if (event.getId() == MENU_SPLIT_HORIZONTAL) { - splitHorizontal(); - return true; - } - - return super.onMenu(event); - } - - /** - * Perform the vertical split. - */ - private void splitVertical() { - TWindow window = getActiveWindow(); - if (!(window instanceof TTerminalWindow)) { - return; - } - - TTerminalWindow tile = (TTerminalWindow) window; - // Give the extra column to the new tile. - int newWidth = (tile.getWidth() + 1) / 2; - int newY = tile.getY() - 1; - int newX = tile.getX() + tile.getWidth() - newWidth; - makeTile(newX, newY, newWidth, tile.getHeight(), tile); - tile.setWidth(tile.getWidth() - newWidth); - tile.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, - tile.getWidth(), tile.getHeight())); - } - - /** - * Perform the horizontal split. - */ - private void splitHorizontal() { - TWindow window = getActiveWindow(); - if (!(window instanceof TTerminalWindow)) { - return; - } - - TTerminalWindow tile = (TTerminalWindow) window; - // Give the extra row to the new tile. - int newHeight = (tile.getHeight() + 1) / 2; - int newY = tile.getY() - 1 + tile.getHeight() - newHeight; - int newX = tile.getX(); - makeTile(newX, newY, tile.getWidth(), newHeight, tile); - tile.setHeight(tile.getHeight() - newHeight); - tile.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, - tile.getWidth(), tile.getHeight())); - } - - /** - * Create a non-resizable non-movable terminal window. - * - * @param x the column number to place the top-left corner at. 0 is the - * left-most column. - * @param y the row number to place the top-left corner at. 0 is the - * top-most column. - * @param width the width of the window - * @param height the height of the window - * @param otherTile the other tile to resize when this window closes - */ - private TTerminalWindow makeTile(int x, int y, int width, int height, - final TTerminalWindow otherTile) { - - // We pass flags to disable the zoom (maximize) button, disable - // "smart" window placement, and set the specific location. - TTerminalWindow tile = new TTerminalWindow(this, x, y, - TWindow.NOZOOMBOX | TWindow.ABSOLUTEXY, - new String[] { "/bin/bash", "--login" }, true) { - - /** - * When this terminal closes, if otherTile is defined then resize - * it to overcover me. - */ - @Override - public void onClose() { - super.onClose(); - - if (otherTile != null) { - if (otherTile.getX() != getX()) { - // Undo the vertical split - otherTile.setX(Math.min(otherTile.getX(), getX())); - otherTile.setWidth(otherTile.getWidth() + getWidth()); - } - if (otherTile.getY() != getY()) { - otherTile.setY(Math.min(otherTile.getY(), getY())); - otherTile.setHeight(otherTile.getHeight() + getHeight()); - } - otherTile.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, - otherTile.getWidth(), otherTile.getHeight())); - } - } - - /** - * Prevent the user from resizing or moving this window. - */ - @Override - public void onMouseDown(final TMouseEvent mouse) { - super.onMouseDown(mouse); - stopMovements(); - } - - /** - * Prevent the user from resizing or moving this window. - */ - @Override - public void onKeypress(final TKeypressEvent keypress) { - super.onKeypress(keypress); - stopMovements(); - } - - /** - * Permit the user to use all of the menu items. - */ - @Override - public void onIdle() { - super.onIdle(); - removeShortcutKeypress(jexer.TKeypress.kbAltT); - removeShortcutKeypress(jexer.TKeypress.kbF6); - } - - }; - - // The initial window size was stock VT100 80x24. Change that now, - // and then call onResize() to notify ptypipe to set the shell's - // window size. - tile.setWidth(width); - tile.setHeight(height); - tile.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, - tile.getWidth(), tile.getHeight())); - - return tile; - } - -} diff --git a/examples/JexerTilingWindowManager2.java b/examples/JexerTilingWindowManager2.java deleted file mode 100644 index 2a1512d..0000000 --- a/examples/JexerTilingWindowManager2.java +++ /dev/null @@ -1,175 +0,0 @@ -import jexer.TAction; -import jexer.TApplication; -import jexer.TDesktop; -import jexer.TTerminalWidget; -import jexer.TSplitPane; -import jexer.TWidget; -import jexer.event.TMenuEvent; -import jexer.menu.TMenu; - -/** - * Implements a simple tiling window manager. A terminal widget is added to - * the desktop, which can be split horizontally or vertically. A close - * action is provided to each window to remove the split when its shell - * exits. - * - * This example shows what can be done with minimal changes to stock Jexer - * widgets. - */ -public class JexerTilingWindowManager2 extends TApplication { - - /** - * Menu item: split the terminal vertically. - */ - private static final int MENU_SPLIT_VERTICAL = 2000; - - /** - * Menu item: split the terminal horizontally. - */ - private static final int MENU_SPLIT_HORIZONTAL = 2001; - /** - * Menu item: recreate the root terminal. - */ - private static final int MENU_RESPAWN_ROOT = 2002; - - /** - * Handle to the root widget. - */ - private TWidget root = null; - - /** - * Main entry point. - */ - public static void main(String [] args) throws Exception { - // For this application, we must use ptypipe so that the terminal - // shells can be aware of their size. - System.setProperty("jexer.TTerminal.ptypipe", "true"); - - // Let's also suppress the status line. - System.setProperty("jexer.hideStatusBar", "true"); - - JexerTilingWindowManager2 jtwm = new JexerTilingWindowManager2(); - (new Thread(jtwm)).start(); - } - - /** - * Public constructor chooses the ECMA-48 / Xterm backend. - */ - public JexerTilingWindowManager2() throws Exception { - super(BackendType.XTERM); - - // The stock tool menu has items for redrawing the screen, opening - // images, and (when using the Swing backend) setting the font. - addToolMenu(); - - // We will have one menu containing a mix of new and stock commands - TMenu tileMenu = addMenu("&Tile"); - - // New commands for this example: split vertical and horizontal. - tileMenu.addItem(MENU_SPLIT_VERTICAL, "&Vertical Split"); - tileMenu.addItem(MENU_SPLIT_HORIZONTAL, "&Horizontal Split"); - tileMenu.addItem(MENU_RESPAWN_ROOT, "&Respawn Root Terminal"); - - // Stock commands: a new shell with resizable window, and exit - // program. - tileMenu.addSeparator(); - tileMenu.addItem(TMenu.MID_SHELL, "&New Windowed Terminal"); - tileMenu.addSeparator(); - tileMenu.addDefaultItem(TMenu.MID_EXIT); - - // TTerminalWidget can request the text-block mouse pointer be - // suppressed, but the default TDesktop will ignore it. Let's set a - // new TDesktop to pass that mouse pointer visibility option to - // TApplication. - setDesktop(new TDesktop(this) { - @Override - public boolean hasHiddenMouse() { - TWidget active = getActiveChild(); - if (active instanceof TTerminalWidget) { - return ((TTerminalWidget) active).hasHiddenMouse(); - } - return false; - } - }); - - // Spin up the root terminal - createRootTerminal(); - } - - /** - * Process menu events. - */ - @Override - protected boolean onMenu(TMenuEvent event) { - TWidget active = getDesktop().getActiveChild(); - TSplitPane split = null; - - switch (event.getId()) { - case MENU_RESPAWN_ROOT: - assert (root == null); - createRootTerminal(); - return true; - - case MENU_SPLIT_VERTICAL: - if (root == null) { - assert (getDesktop().getActiveChild() == null); - createRootTerminal(); - return true; - } - split = active.splitVertical(false, createTerminal()); - if (active == root) { - root = split; - } - return true; - - case MENU_SPLIT_HORIZONTAL: - if (root == null) { - assert (getDesktop().getActiveChild() == null); - createRootTerminal(); - return true; - } - split = active.splitHorizontal(false, createTerminal()); - if (active == root) { - root = split; - } - return true; - - default: - return super.onMenu(event); - } - - } - - /** - * Create the root terminal. - */ - private void createRootTerminal() { - assert (root == null); - disableMenuItem(MENU_RESPAWN_ROOT); - root = createTerminal(); - } - - /** - * Create a new terminal. - * - * @return the new terminal - */ - private TWidget createTerminal() { - return new TTerminalWidget(getDesktop(), 0, 0, - getDesktop().getWidth(), getDesktop().getHeight(), - new TAction() { - public void DO() { - if (source.getParent() instanceof TSplitPane) { - ((TSplitPane) source.getParent()).removeSplit(source, - true); - } else { - source.getApplication().enableMenuItem( - MENU_RESPAWN_ROOT); - source.remove(); - root = null; - } - } - }); - } - -} diff --git a/examples/MyApplication.java b/examples/MyApplication.java deleted file mode 100644 index 2af892c..0000000 --- a/examples/MyApplication.java +++ /dev/null @@ -1,18 +0,0 @@ -import jexer.TApplication; - -public class MyApplication extends TApplication { - - public MyApplication() throws Exception { - super(BackendType.XTERM); - - // Create standard menus for Tool, File, and Window. - addToolMenu(); - addFileMenu(); - addWindowMenu(); - } - - public static void main(String [] args) throws Exception { - MyApplication app = new MyApplication(); - app.run(); - } -} diff --git a/examples/imgls b/examples/imgls new file mode 100755 index 0000000..99bbb9c --- /dev/null +++ b/examples/imgls @@ -0,0 +1,64 @@ +#!/bin/bash + +# This is a modified version of the 'imgls' from iTerm2 located at +# https://iterm2.com/utilities/imgls, modified to emit images with the +# Jexer image protocol. + +# tmux requires unrecognized OSC sequences to be wrapped with DCS tmux; +# ST, and for all ESCs in to be replaced with ESC ESC. It +# only accepts ESC backslash for ST. +function print_osc() { + if [ x"$TERM" = "xscreen" ] ; then + printf "\033Ptmux;\033\033]" + else + printf "\033]" + fi +} + +function check_dependency() { + if ! (builtin command -V "$1" > /dev/null 2>& 1); then + echo "imgcat: missing dependency: can't find $1" 1>& 2 + exit 1 + fi +} + +# More of the tmux workaround described above. +function print_st() { + if [ x"$TERM" = "xscreen" ] ; then + printf "\a\033\\" + else + printf "\a" + fi +} + +function list_file() { + fn=$1 + test -f "$fn" || return 0 + + if [ "${fn: -4}" == ".png" ]; then + print_osc + printf '444;1;1;' + base64 < "$fn" + print_st + elif [ "${fn: -4}" == ".jpg" ]; then + print_osc + printf '444;2;1;' + base64 < "$fn" + print_st + fi +} + +check_dependency base64 + +if [ $# -eq 0 ]; then + for fn in * + do + list_file "$fn" + done < <(ls -ls) +else + for fn in "$@" + do + list_file "$fn" + done +fi + diff --git a/help/HelpFile.java b/help/HelpFile.java new file mode 100644 index 0000000..7a6f49e --- /dev/null +++ b/help/HelpFile.java @@ -0,0 +1,381 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.help; + +import java.io.InputStream; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.ResourceBundle; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.ParserConfigurationException; +import org.xml.sax.SAXException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * A HelpFile is a collection of Topics with a table of contents and index of + * relevant terms. + */ +public class HelpFile { + + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(HelpFile.class.getName()); + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The XML factory. + */ + private static DocumentBuilder domBuilder; + + /** + * The map of topics by title. + */ + private HashMap topicsByTitle; + + /** + * The map of topics by index key term. + */ + private HashMap topicsByTerm; + + /** + * The special "table of contents" topic. + */ + private Topic tableOfContents; + + /** + * The special "index" topic. + */ + private Topic index; + + /** + * The name of this help file. + */ + private String name = ""; + + /** + * The version of this help file. + */ + private String version = ""; + + /** + * The help file author. + */ + private String author = ""; + + /** + * The help file copyright/written by date. + */ + private String date = ""; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + // ------------------------------------------------------------------------ + // HelpFile --------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Load a help file from an input stream. + * + * @param input the input strem + * @throws IOException if an I/O error occurs + * @throws ParserConfigurationException if no XML parser is available + * @throws SAXException if XML parsing fails + */ + public void load(final InputStream input) throws IOException, + ParserConfigurationException, SAXException { + + topicsByTitle = new HashMap(); + topicsByTerm = new HashMap(); + + try { + loadTopics(input); + } finally { + // Always generate the TOC and Index from what was read. + generateTableOfContents(); + generateIndex(); + } + } + + /** + * Get a topic by title. + * + * @param title the title for the topic + * @return the topic, or the "not found" topic if title is not found + */ + public Topic getTopic(final String title) { + Topic topic = topicsByTitle.get(title); + if (topic == null) { + return Topic.NOT_FOUND; + } + return topic; + } + + /** + * Get the special "search results" topic. + * + * @param searchString a regular expression search string + * @return an index topic containing topics with text that matches the + * search string + */ + public Topic getSearchResults(final String searchString) { + List allTopics = new ArrayList(); + allTopics.addAll(topicsByTitle.values()); + Collections.sort(allTopics); + + List results = new ArrayList(); + Pattern pattern = Pattern.compile(searchString); + Pattern patternLower = Pattern.compile(searchString.toLowerCase()); + + for (Topic topic: allTopics) { + Matcher match = pattern.matcher(topic.getText().toLowerCase()); + if (match.find()) { + results.add(topic); + continue; + } + match = pattern.matcher(topic.getTitle().toLowerCase()); + if (match.find()) { + results.add(topic); + continue; + } + match = patternLower.matcher(topic.getText().toLowerCase()); + if (match.find()) { + results.add(topic); + continue; + } + match = patternLower.matcher(topic.getTitle().toLowerCase()); + if (match.find()) { + results.add(topic); + continue; + } + } + + StringBuilder text = new StringBuilder(); + int wordIndex = 0; + List links = new ArrayList(); + for (Topic topic: results) { + text.append(topic.getTitle()); + text.append("\n\n"); + + Link link = new Link(topic.getTitle(), topic.getTitle(), wordIndex); + wordIndex += link.getWordCount(); + links.add(link); + } + + return new Topic(MessageFormat.format(i18n.getString("searchResults"), + searchString), text.toString(), links); + } + + /** + * Get the special "table of contents" topic. + * + * @return the table of contents topic + */ + public Topic getTableOfContents() { + return tableOfContents; + } + + /** + * Get the special "index" topic. + * + * @return the index topic + */ + public Topic getIndex() { + return index; + } + + /** + * Generate the table of contents topic. + */ + private void generateTableOfContents() { + List allTopics = new ArrayList(); + allTopics.addAll(topicsByTitle.values()); + Collections.sort(allTopics); + + StringBuilder text = new StringBuilder(); + int wordIndex = 0; + List links = new ArrayList(); + for (Topic topic: allTopics) { + text.append(topic.getTitle()); + text.append("\n\n"); + + Link link = new Link(topic.getTitle(), topic.getTitle(), wordIndex); + wordIndex += link.getWordCount(); + links.add(link); + } + + tableOfContents = new Topic(i18n.getString("tableOfContents"), + text.toString(), links); + } + + /** + * Generate the index topic. + */ + private void generateIndex() { + List allTopics = new ArrayList(); + allTopics.addAll(topicsByTitle.values()); + + HashMap> allKeys; + allKeys = new HashMap>(); + for (Topic topic: allTopics) { + for (String key: topic.getIndexKeys()) { + key = key.toLowerCase(); + ArrayList topics = allKeys.get(key); + if (topics == null) { + topics = new ArrayList(); + allKeys.put(key, topics); + } + topics.add(topic); + } + } + List keys = new ArrayList(); + keys.addAll(allKeys.keySet()); + Collections.sort(keys); + + StringBuilder text = new StringBuilder(); + int wordIndex = 0; + List links = new ArrayList(); + + for (String key: keys) { + List topics = allKeys.get(key); + assert (topics != null); + for (Topic topic: topics) { + String line = String.format("%15s %15s", key, topic.getTitle()); + text.append(line); + text.append("\n\n"); + + wordIndex += key.split("\\s+").length; + Link link = new Link(topic.getTitle(), topic.getTitle(), wordIndex); + wordIndex += link.getWordCount(); + links.add(link); + } + } + + index = new Topic(i18n.getString("index"), text.toString(), links); + } + + /** + * Load topics from a help file into the topics pool. + * + * @param input the input strem + * @throws IOException if an I/O error occurs + * @throws ParserConfigurationException if no XML parser is available + * @throws SAXException if XML parsing fails + */ + private void loadTopics(final InputStream input) throws IOException, + ParserConfigurationException, SAXException { + + if (domBuilder == null) { + DocumentBuilderFactory dbFactory = DocumentBuilderFactory. + newInstance(); + domBuilder = dbFactory.newDocumentBuilder(); + } + Document doc = domBuilder.parse(input); + + // Get the document's root XML node + Node root = doc.getChildNodes().item(0); + NodeList level1 = root.getChildNodes(); + for (int i = 0; i < level1.getLength(); i++) { + Node node = level1.item(i); + String name = node.getNodeName(); + String value = node.getTextContent(); + + if (name.equals("name")) { + this.name = value; + } + if (name.equals("version")) { + this.version = value; + } + if (name.equals("author")) { + this.author = value; + } + if (name.equals("date")) { + this.date = value; + } + if (name.equals("topics")) { + NodeList topics = node.getChildNodes(); + for (int j = 0; j < topics.getLength(); j++) { + Node topic = topics.item(j); + addTopic(topic); + } + } + } + } + + /** + * Add a topic to this help file. + * + * @param xmlNode the topic XML node + * @throws IOException if a java.io operation throws + */ + private void addTopic(final Node xmlNode) throws IOException { + String title = ""; + String text = ""; + + NamedNodeMap attributes = xmlNode.getAttributes(); + if (attributes != null) { + for (int i = 0; i < attributes.getLength(); i++) { + Node attr = attributes.item(i); + if (attr.getNodeName().equals("title")) { + title = attr.getNodeValue().trim(); + } + } + } + NodeList level2 = xmlNode.getChildNodes(); + for (int i = 0; i < level2.getLength(); i++) { + Node node = level2.item(i); + String nodeName = node.getNodeName(); + String nodeValue = node.getTextContent(); + if (nodeName.equals("text")) { + text = nodeValue.trim(); + } + } + if (title.length() > 0) { + Topic topic = new Topic(title, text); + topicsByTitle.put(title, topic); + } + } + +} diff --git a/help/HelpFile.properties b/help/HelpFile.properties new file mode 100644 index 0000000..803961b --- /dev/null +++ b/help/HelpFile.properties @@ -0,0 +1,3 @@ +tableOfContents=Table Of Contents +index=Index +searchResults=Search Results - {0} diff --git a/help/Link.java b/help/Link.java new file mode 100644 index 0000000..665381c --- /dev/null +++ b/help/Link.java @@ -0,0 +1,135 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.help; + +import java.util.HashSet; +import java.util.Set; +import java.util.ResourceBundle; + +/** + * A Link is a section of text with a reference to a Topic. + */ +public class Link { + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The topic id that this link points to. + */ + private String topic; + + /** + * The text inside the link tag. + */ + private String text; + + /** + * The number of words in this link. + */ + private int wordCount; + + /** + * The word number (from the beginning of topic text) that corresponds to + * the first word of this link. + */ + private int index; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + * + * @param topic the topic to point to + * @param text the text inside the link tag + * @param index the word count index + */ + public Link(final String topic, final String text, final int index) { + this.topic = topic; + this.text = text; + this.index = index; + this.wordCount = text.split("\\s+").length; + } + + // ------------------------------------------------------------------------ + // Link ------------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Get the topic. + * + * @return the topic + */ + public String getTopic() { + return topic; + } + + /** + * Get the link text. + * + * @return the text inside the link tag + */ + public String getText() { + return text; + } + + /** + * Get the word index for this link. + * + * @return the word number (from the beginning of topic text) that + * corresponds to the first word of this link + */ + public int getIndex() { + return index; + } + + /** + * Get the number of words in this link. + * + * @return the number of words in this link + */ + public int getWordCount() { + return wordCount; + } + + /** + * Generate a human-readable string for this widget. + * + * @return a human-readable string + */ + @Override + public String toString() { + return String.format("%s(%8x) topic %s link text %s word # %d count %d", + getClass().getName(), hashCode(), topic, text, index, wordCount); + } + +} diff --git a/help/THelpText.java b/help/THelpText.java new file mode 100644 index 0000000..2e0afcf --- /dev/null +++ b/help/THelpText.java @@ -0,0 +1,389 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.help; + +import java.util.ArrayList; +import java.util.List; + +import jexer.THelpWindow; +import jexer.TScrollableWidget; +import jexer.TVScroller; +import jexer.TWidget; +import jexer.bits.CellAttributes; +import jexer.bits.StringUtils; +import jexer.event.TKeypressEvent; +import jexer.event.TMouseEvent; +import static jexer.TKeypress.*; + +/** + * THelpText displays help text with clickable links in a scrollable text + * area. It reflows automatically on resize. + */ +public class THelpText extends TScrollableWidget { + + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The number of lines to scroll on mouse wheel up/down. + */ + private static final int wheelScrollSize = 3; + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The paragraphs in this text box. + */ + private List paragraphs; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + * + * @param parent parent widget + * @param topic the topic to display + * @param x column relative to parent + * @param y row relative to parent + * @param width width of text area + * @param height height of text area + */ + public THelpText(final THelpWindow parent, final Topic topic, final int x, + final int y, final int width, final int height) { + + // Set parent and window + super(parent, x, y, width, height); + + vScroller = new TVScroller(this, getWidth() - 1, 0, + Math.max(1, getHeight())); + + setTopic(topic); + } + + // ------------------------------------------------------------------------ + // 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); + } + } + + /** + * 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); + if (hScroller != null) { + hScroller.setY(getHeight() - 1); + } + if (vScroller != null) { + vScroller.setHeight(Math.max(1, getHeight())); + } + } + + /** + * Handle mouse press events. + * + * @param mouse mouse button press event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + // Pass to children + super.onMouseDown(mouse); + + if (mouse.isMouseWheelUp()) { + for (int i = 0; i < wheelScrollSize; i++) { + vScroller.decrement(); + } + reflowData(); + return; + } + if (mouse.isMouseWheelDown()) { + for (int i = 0; i < wheelScrollSize; i++) { + vScroller.increment(); + } + reflowData(); + return; + } + + // User clicked on a paragraph, update the scrollbar accordingly. + for (int i = 0; i < paragraphs.size(); i++) { + if (paragraphs.get(i).isActive()) { + setVerticalValue(i); + return; + } + } + } + + /** + * Handle keystrokes. + * + * @param keypress keystroke event + */ + @Override + public void onKeypress(final TKeypressEvent keypress) { + if (keypress.equals(kbTab)) { + getParent().switchWidget(true); + } else if (keypress.equals(kbShiftTab)) { + getParent().switchWidget(false); + } else if (keypress.equals(kbUp)) { + if (!paragraphs.get(getVerticalValue()).up()) { + vScroller.decrement(); + reflowData(); + } + } else if (keypress.equals(kbDown)) { + if (!paragraphs.get(getVerticalValue()).down()) { + vScroller.increment(); + reflowData(); + } + } else if (keypress.equals(kbPgUp)) { + vScroller.bigDecrement(); + reflowData(); + } else if (keypress.equals(kbPgDn)) { + vScroller.bigIncrement(); + reflowData(); + } else if (keypress.equals(kbHome)) { + vScroller.toTop(); + reflowData(); + } else if (keypress.equals(kbEnd)) { + vScroller.toBottom(); + reflowData(); + } else { + // Pass other keys on + super.onKeypress(keypress); + } + } + + /** + * Place the scrollbars on the edge of this widget, and adjust bigChange + * to match the new size. This is called by onResize(). + */ + protected void placeScrollbars() { + if (hScroller != null) { + hScroller.setY(getHeight() - 1); + hScroller.setWidth(getWidth() - 1); + hScroller.setBigChange(getWidth() - 1); + } + if (vScroller != null) { + vScroller.setX(getWidth() - 1); + vScroller.setHeight(getHeight()); + vScroller.setBigChange(getHeight()); + } + } + + /** + * Resize text and scrollbars for a new width/height. + */ + @Override + public void reflowData() { + for (TParagraph paragraph: paragraphs) { + paragraph.setWidth(getWidth() - 1); + paragraph.reflowData(); + } + + int top = getVerticalValue(); + int paragraphsHeight = 0; + for (TParagraph paragraph: paragraphs) { + paragraphsHeight += paragraph.getHeight(); + } + if (paragraphsHeight <= getHeight()) { + // All paragraphs fit in the window. + int y = 0; + for (int i = 0; i < paragraphs.size(); i++) { + paragraphs.get(i).setEnabled(true); + paragraphs.get(i).setVisible(true); + paragraphs.get(i).setY(y); + y += paragraphs.get(i).getHeight(); + } + activate(paragraphs.get(getVerticalValue())); + return; + } + + /* + * Some paragraphs will not fit in the window. Find the number of + * rows needed to display from the current vertical position to the + * end: + * + * - If this meets or exceeds the available height, then draw from + * the vertical position to the number of visible rows. + * + * - If this is less than the available height, back up until + * meeting/exceeding the height, and draw from there to the end. + * + */ + int rowsNeeded = 0; + for (int i = getVerticalValue(); i <= getBottomValue(); i++) { + rowsNeeded += paragraphs.get(i).getHeight(); + } + while (rowsNeeded < getHeight()) { + // Decrease top until we meet/exceed the visible display. + if (top == getTopValue()) { + break; + } + top--; + rowsNeeded += paragraphs.get(top).getHeight(); + } + + // All set, now disable all paragraphs except the visible ones. + for (TParagraph paragraph: paragraphs) { + paragraph.setEnabled(false); + paragraph.setVisible(false); + paragraph.setY(-1); + } + int y = 0; + for (int i = top; (i <= getBottomValue()) && (y < getHeight()); i++) { + paragraphs.get(i).setEnabled(true); + paragraphs.get(i).setVisible(true); + paragraphs.get(i).setY(y); + y += paragraphs.get(i).getHeight(); + } + activate(paragraphs.get(getVerticalValue())); + } + + /** + * Draw the text box. + */ + @Override + public void draw() { + // Setup my color + CellAttributes color = getTheme().getColor("thelpwindow.text"); + for (int y = 0; y < getHeight(); y++) { + hLineXY(0, y, getWidth(), ' ', color); + } + } + + // ------------------------------------------------------------------------ + // THelpText -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Set the topic. + * + * @param topic new topic to display + */ + public void setTopic(final Topic topic) { + setTopic(topic, true); + } + + /** + * Set the topic. + * + * @param topic new topic to display + * @param separator if true, separate paragraphs + */ + public void setTopic(final Topic topic, final boolean separator) { + + if (paragraphs != null) { + getChildren().removeAll(paragraphs); + } + paragraphs = new ArrayList(); + + // Add title paragraph at top. We explicitly set the separator to + // false to achieve the underscore effect. + List title = new ArrayList(); + title.add(new TWord(topic.getTitle(), null)); + TParagraph titleParagraph = new TParagraph(this, title); + titleParagraph.separator = false; + paragraphs.add(titleParagraph); + title = new ArrayList(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < topic.getTitle().length(); i++) { + sb.append('\u2580'); + } + title.add(new TWord(sb.toString(), null)); + titleParagraph = new TParagraph(this, title); + paragraphs.add(titleParagraph); + + // Now add the actual text as paragraphs. + int wordIndex = 0; + + // Break up text into paragraphs + String [] blocks = topic.getText().split("\n\n"); + for (String block: blocks) { + List words = new ArrayList(); + String [] lines = block.split("\n"); + for (String line: lines) { + line = line.trim(); + // System.err.println("line: " + line); + String [] wordTokens = line.split("\\s+"); + for (int i = 0; i < wordTokens.length; i++) { + String wordStr = wordTokens[i].trim(); + Link wordLink = null; + for (Link link: topic.getLinks()) { + if ((i + wordIndex >= link.getIndex()) + && (i + wordIndex < link.getIndex() + link.getWordCount()) + ) { + // This word is part of a link. + wordLink = link; + wordStr = link.getText(); + i += link.getWordCount() - 1; + break; + } + } + TWord word = new TWord(wordStr, wordLink); + /* + System.err.println("add word at " + (i + wordIndex) + " : " + + wordStr + " " + wordLink); + */ + words.add(word); + } // for (int i = 0; i < words.length; i++) + wordIndex += wordTokens.length; + } // for (String line: lines) + TParagraph paragraph = new TParagraph(this, words); + paragraph.separator = separator; + paragraphs.add(paragraph); + } // for (String block: blocks) + + setBottomValue(paragraphs.size() - 1); + setVerticalValue(0); + reflowData(); + } + +} diff --git a/help/TParagraph.java b/help/TParagraph.java new file mode 100644 index 0000000..04559da --- /dev/null +++ b/help/TParagraph.java @@ -0,0 +1,175 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.help; + +import java.util.List; + +import jexer.TWidget; + +/** + * TParagraph contains a reflowable collection of TWords, some of which are + * clickable links. + */ +public class TParagraph extends TWidget { + + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Topic text and links converted to words. + */ + private List words; + + /** + * If true, add one row to height as a paragraph separator. Note package + * private access. + */ + boolean separator = true; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + * + * @param parent parent widget + * @param words the pieces of the paragraph to display + */ + public TParagraph(final THelpText parent, final List words) { + + // Set parent and window + super(parent, 0, 0, parent.getWidth() - 1, 1); + + this.words = words; + for (TWord word: words) { + word.setParent(this, false); + } + + reflowData(); + } + + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + // ------------------------------------------------------------------------ + // TParagraph ------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Reposition the words in this paragraph to reflect the new width, and + * set the paragraph height. + */ + public void reflowData() { + int x = 0; + int y = 0; + for (TWord word: words) { + if (x + word.getWidth() >= getWidth()) { + x = 0; + y++; + } + word.setX(x); + word.setY(y); + x += word.getWidth() + 1; + } + if (separator) { + setHeight(y + 2); + } else { + setHeight(y + 1); + } + } + + /** + * Try to select a previous link. + * + * @return true if there was a previous link in this paragraph to select + */ + public boolean up() { + if (words.size() == 0) { + return false; + } + if (getActiveChild() == this) { + // No selectable links + return false; + } + TWord firstWord = null; + TWord lastWord = null; + for (TWord word: words) { + if (word.isEnabled()) { + if (firstWord == null) { + firstWord = word; + } + lastWord = word; + } + } + if (getActiveChild() == firstWord) { + return false; + } + switchWidget(false); + return true; + } + + /** + * Try to select a next link. + * + * @return true if there was a next link in this paragraph to select + */ + public boolean down() { + if (words.size() == 0) { + return false; + } + if (getActiveChild() == this) { + // No selectable links + return false; + } + TWord firstWord = null; + TWord lastWord = null; + for (TWord word: words) { + if (word.isEnabled()) { + if (firstWord == null) { + firstWord = word; + } + lastWord = word; + } + } + if (getActiveChild() == lastWord) { + return false; + } + switchWidget(true); + return true; + } + + +} diff --git a/help/TWord.java b/help/TWord.java new file mode 100644 index 0000000..d46a22e --- /dev/null +++ b/help/TWord.java @@ -0,0 +1,143 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.help; + +import jexer.THelpWindow; +import jexer.TWidget; +import jexer.bits.CellAttributes; +import jexer.bits.StringUtils; +import jexer.event.TKeypressEvent; +import jexer.event.TMouseEvent; +import static jexer.TKeypress.*; + +/** + * TWord contains either a string to display or a clickable link. + */ +public class TWord extends TWidget { + + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The word(s) to display. + */ + private String words; + + /** + * Link to another Topic. + */ + private Link link; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + * + * @param words the words to display + * @param link link to other topic, or null + */ + public TWord(final String words, final Link link) { + + // TWord is created by THelpText before the TParagraph is belongs to + // is created, so pass null as parent for now. + super(null, 0, 0, StringUtils.width(words), 1); + + this.words = words; + this.link = link; + + // Don't make text-only words "active". + if (link == null) { + setEnabled(false); + } + } + + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Handle mouse press events. + * + * @param mouse mouse button press event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + if (mouse.isMouse1()) { + if (link != null) { + ((THelpWindow) getWindow()).setHelpTopic(link.getTopic()); + } + } + } + + /** + * Handle keystrokes. + * + * @param keypress keystroke event + */ + @Override + public void onKeypress(final TKeypressEvent keypress) { + if (keypress.equals(kbEnter)) { + if (link != null) { + ((THelpWindow) getWindow()).setHelpTopic(link.getTopic()); + } + } + } + + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw the words. + */ + @Override + public void draw() { + CellAttributes color = getTheme().getColor("thelpwindow.text"); + if (link != null) { + if (isAbsoluteActive()) { + color = getTheme().getColor("thelpwindow.link.active"); + } else { + color = getTheme().getColor("thelpwindow.link"); + } + } + putStringXY(0, 0, words, color); + } + + // ------------------------------------------------------------------------ + // TWord ------------------------------------------------------------------ + // ------------------------------------------------------------------------ + +} diff --git a/help/Topic.java b/help/Topic.java new file mode 100644 index 0000000..8c0bc71 --- /dev/null +++ b/help/Topic.java @@ -0,0 +1,339 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.help; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.ResourceBundle; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A Topic is a page of help text with a title and possibly links to other + * Topics. + */ +public class Topic implements Comparable { + + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(Topic.class.getName()); + + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The "not found" topic to display when a key or index term does not + * have an associated topic. Note package private access. + */ + static Topic NOT_FOUND = null; + + /** + * The regex for identifying index tags. + */ + private static final String INDEX_REGEX_STR = "\\#\\{([^\\}]*)\\}"; + + /** + * The regex for identifying link tags. + */ + private static final String LINK_REGEX_STR = "\\[([^\\]]*)\\]\\(([^\\)]*)\\)"; + + /** + * The regex for identifying words. + */ + private static final String WORD_REGEX_STR = "[ \\t]+"; + + /** + * The index match regex. + */ + private static Pattern INDEX_REGEX; + + /** + * The link match regex. + */ + private static Pattern LINK_REGEX; + + /** + * The word match regex. + */ + private static Pattern WORD_REGEX; + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The title for this topic. + */ + private String title; + + /** + * The text for this topic. + */ + private String text; + + /** + * The index keys in this topic. + */ + private Set indexKeys = new HashSet(); + + /** + * The links in this topic. + */ + private List links = new ArrayList(); + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Static constructor. + */ + static { + try { + INDEX_REGEX = Pattern.compile(INDEX_REGEX_STR); + LINK_REGEX = Pattern.compile(LINK_REGEX_STR); + WORD_REGEX = Pattern.compile(WORD_REGEX_STR); + + NOT_FOUND = new Topic(i18n.getString("topicNotFoundTitle"), + i18n.getString("topicNotFoundText")); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Public constructor. + * + * @param title the topic title + * @param text the topic text + */ + public Topic(final String title, final String text) { + this.title = title; + processText(text); + } + + /** + * Package private constructor. + * + * @param title the topic title + * @param text the topic text + * @param links links to add after processing text + */ + Topic(final String title, final String text, final List links) { + this.title = title; + processText(text); + this.links.addAll(links); + } + + // ------------------------------------------------------------------------ + // Topic ------------------------------------------------------------------ + // ------------------------------------------------------------------------ + + /** + * Get the topic title. + * + * @return the title + */ + public String getTitle() { + return title; + } + + /** + * Get the topic text. + * + * @return the text + */ + public String getText() { + return text; + } + + /** + * Get the index keys. + * + * @return the keys + */ + public Set getIndexKeys() { + return indexKeys; + } + + /** + * Get the links. + * + * @return the links + */ + public List getLinks() { + return links; + } + + /** + * Comparison operator. + * + * @param that another Topic instance + * @return comparison by topic title + */ + public int compareTo(final Topic that) { + return title.compareTo(that.title); + } + + /** + * Generate a human-readable string for this widget. + * + * @return a human-readable string + */ + @Override + public String toString() { + return String.format("%s(%8x) topic %s text %s links %s indexKeys %s", + getClass().getName(), hashCode(), title, text, links, indexKeys); + } + + /** + * Process a string through the regexes, building up the indexes and + * links. + * + * @param text the text to process + */ + private void processText(final String text) { + StringBuilder sb = new StringBuilder(); + String [] lines = text.split("\n"); + int wordIndex = 0; + for (String line: lines) { + line = line.trim(); + + String cleanLine = ""; + + // System.err.println("LINE " + wordIndex + " : '" + line + "'"); + + Matcher index = INDEX_REGEX.matcher(line); + int start = 0; + while (index.find()) { + cleanLine += line.substring(start, index.start()); + String key = index.group(1); + cleanLine += key; + start = index.end(); + // System.err.println("ADD KEY: " + key); + indexKeys.add(key); + } + cleanLine += line.substring(start); + + line = cleanLine; + cleanLine = ""; + + /* + System.err.println("line after removing #{index} tags: " + + wordIndex + " '" + line + "'"); + */ + + Matcher link = LINK_REGEX.matcher(line); + start = 0; + + boolean hasLink = link.find(); + + // System.err.println("hasLink " + hasLink); + + while (true) { + + if (hasLink == false) { + cleanLine += line.substring(start); + + String remaining = line.substring(start).trim(); + Matcher word = WORD_REGEX.matcher(remaining); + while (word.find()) { + // System.err.println("word.find() true"); + wordIndex++; + } + if (remaining.length() > 0) { + // The last word on the line. + wordIndex++; + } + break; + } + + assert (hasLink == true); + + int linkWordIndex = link.start(); + int cleanLineStart = cleanLine.length(); + cleanLine += line.substring(start, linkWordIndex); + String linkText = link.group(1); + String topic = link.group(2); + cleanLine += linkText; + start = link.end(); + + // Increment wordIndex until we reach the first word of + // the link text. + Matcher word = WORD_REGEX.matcher(cleanLine. + substring(cleanLineStart)); + while (word.find()) { + if (word.end() <= linkWordIndex) { + wordIndex++; + } else { + // We have found the word that matches the first + // word of link text, bail out. + break; + } + } + /* + System.err.println("ADD LINK --> " + topic + ": '" + + linkText + "' word index " + wordIndex); + */ + links.add(new Link(topic, linkText, wordIndex)); + + // The rest of the words in the link text. + while (word.find()) { + wordIndex++; + } + // The final word after the last whitespace. + wordIndex++; + + hasLink = link.find(); + if (hasLink) { + wordIndex += 3; + } + } + + + /* + System.err.println("line after removing [link](...) tags: '" + + cleanLine + "'"); + */ + + // Append the entire line. + sb.append(cleanLine); + sb.append("\n"); + + this.text = sb.toString(); + + } // for (String line: lines) + + } + +} diff --git a/help/Topic.properties b/help/Topic.properties new file mode 100644 index 0000000..1c8de6f --- /dev/null +++ b/help/Topic.properties @@ -0,0 +1,2 @@ +topicNotFoundTitle=Topic Not Found +topicNotFoundText=The help topic was not found. diff --git a/help/package-info.java b/help/package-info.java new file mode 100644 index 0000000..409c370 --- /dev/null +++ b/help/package-info.java @@ -0,0 +1,33 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ + +/** + * Online help system. + */ +package jexer.help; diff --git a/src/jexer/io/ReadTimeoutException.java b/io/ReadTimeoutException.java similarity index 100% rename from src/jexer/io/ReadTimeoutException.java rename to io/ReadTimeoutException.java diff --git a/src/jexer/io/TimeoutInputStream.java b/io/TimeoutInputStream.java similarity index 99% rename from src/jexer/io/TimeoutInputStream.java rename to io/TimeoutInputStream.java index 3d8cdb0..70faff4 100644 --- a/src/jexer/io/TimeoutInputStream.java +++ b/io/TimeoutInputStream.java @@ -244,7 +244,7 @@ public class TimeoutInputStream extends InputStream { if (timeoutMillis == 0) { // Block on the read(). - return stream.read(b); + return stream.read(b, off, len); } int remaining = len; diff --git a/src/jexer/io/package-info.java b/io/package-info.java similarity index 100% rename from src/jexer/io/package-info.java rename to io/package-info.java diff --git a/src/jexer/layout/BoxLayoutManager.java b/layout/BoxLayoutManager.java similarity index 100% rename from src/jexer/layout/BoxLayoutManager.java rename to layout/BoxLayoutManager.java diff --git a/src/jexer/layout/LayoutManager.java b/layout/LayoutManager.java similarity index 100% rename from src/jexer/layout/LayoutManager.java rename to layout/LayoutManager.java diff --git a/src/jexer/layout/StretchLayoutManager.java b/layout/StretchLayoutManager.java similarity index 97% rename from src/jexer/layout/StretchLayoutManager.java rename to layout/StretchLayoutManager.java index ee2bf5a..4bcb0cf 100644 --- a/src/jexer/layout/StretchLayoutManager.java +++ b/layout/StretchLayoutManager.java @@ -146,11 +146,11 @@ public class StretchLayoutManager implements LayoutManager { */ 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 --git a/src/jexer/layout/package-info.java b/layout/package-info.java similarity index 100% rename from src/jexer/layout/package-info.java rename to layout/package-info.java diff --git a/src/jexer/menu/TMenu.java b/menu/TMenu.java similarity index 94% rename from src/jexer/menu/TMenu.java rename to menu/TMenu.java index 6d746df..6a875c7 100644 --- a/src/jexer/menu/TMenu.java +++ b/menu/TMenu.java @@ -72,10 +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 @@ public class TMenu extends TWindow { */ private MnemonicString mnemonic; + /** + * If true, draw icons with menu items. Note package private access. + */ + boolean useIcons = false; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -182,6 +189,11 @@ public class TMenu extends TWindow { setHeight(2); setActive(false); + + if (System.getProperty("jexer.menuIcons", "false").equals("true")) { + useIcons = true; + } + } // ------------------------------------------------------------------------ @@ -446,7 +458,7 @@ public class TMenu extends TWindow { final boolean enabled) { assert (id >= 1024); - return addItemInternal(id, label, null, enabled); + return addItemInternal(id, label, null, enabled, -1); } /** @@ -492,7 +504,7 @@ public class TMenu extends TWindow { 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 @@ public class TMenu extends TWindow { * @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 @@ public class TMenu extends TWindow { String label; TKeypress key = null; + int icon = -1; boolean checkable = false; boolean checked = false; @@ -558,6 +572,7 @@ public class TMenu extends TWindow { case MID_REPAINT: label = i18n.getString("menuRepaintDesktop"); + icon = 0x1F3A8; break; case MID_VIEW_IMAGE: @@ -570,41 +585,56 @@ public class TMenu extends TWindow { 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 @@ public class TMenu extends TWindow { break; case MID_CASCADE: label = i18n.getString("menuWindowCascade"); + icon = 0x1F5D7; break; case MID_CLOSE_ALL: label = i18n.getString("menuWindowCloseAll"); @@ -629,18 +660,22 @@ public class TMenu extends TWindow { 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 @@ public class TMenu extends TWindow { 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 --git a/src/jexer/menu/TMenu.properties b/menu/TMenu.properties similarity index 98% rename from src/jexer/menu/TMenu.properties rename to menu/TMenu.properties index 4a0f8e6..692293e 100644 --- a/src/jexer/menu/TMenu.properties +++ b/menu/TMenu.properties @@ -2,6 +2,8 @@ menuNew=&New menuExit=E&xit menuShell=O&S Shell menuOpen=&Open +menuUndo=&Undo +menuRedo=&Redo menuCut=Cu&t menuCopy=&Copy menuPaste=&Paste diff --git a/src/jexer/menu/TMenuItem.java b/menu/TMenuItem.java similarity index 85% rename from src/jexer/menu/TMenuItem.java rename to menu/TMenuItem.java index d9dfc2a..b478059 100644 --- a/src/jexer/menu/TMenuItem.java +++ b/menu/TMenuItem.java @@ -80,6 +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 @@ public class TMenuItem extends TWidget { 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 @@ public class TMenuItem extends TWidget { 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 @@ public class TMenuItem extends TWidget { } } + 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 @@ public class TMenuItem extends TWidget { 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 --git a/src/jexer/menu/TMenuSeparator.java b/menu/TMenuSeparator.java similarity index 100% rename from src/jexer/menu/TMenuSeparator.java rename to menu/TMenuSeparator.java diff --git a/src/jexer/menu/TSubMenu.java b/menu/TSubMenu.java similarity index 90% rename from src/jexer/menu/TSubMenu.java rename to menu/TSubMenu.java index e285c5a..be281b5 100644 --- a/src/jexer/menu/TSubMenu.java +++ b/menu/TSubMenu.java @@ -212,6 +212,21 @@ public class TSubMenu extends TMenuItem { 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 @@ public class TSubMenu extends TMenuItem { 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 --git a/src/jexer/menu/package-info.java b/menu/package-info.java similarity index 100% rename from src/jexer/menu/package-info.java rename to menu/package-info.java diff --git a/src/jexer/net/TelnetInputStream.java b/net/TelnetInputStream.java similarity index 100% rename from src/jexer/net/TelnetInputStream.java rename to net/TelnetInputStream.java diff --git a/src/jexer/net/TelnetOutputStream.java b/net/TelnetOutputStream.java similarity index 100% rename from src/jexer/net/TelnetOutputStream.java rename to net/TelnetOutputStream.java diff --git a/src/jexer/net/TelnetServerSocket.java b/net/TelnetServerSocket.java similarity index 100% rename from src/jexer/net/TelnetServerSocket.java rename to net/TelnetServerSocket.java diff --git a/src/jexer/net/TelnetSocket.java b/net/TelnetSocket.java similarity index 100% rename from src/jexer/net/TelnetSocket.java rename to net/TelnetSocket.java diff --git a/src/jexer/net/package-info.java b/net/package-info.java similarity index 100% rename from src/jexer/net/package-info.java rename to net/package-info.java diff --git a/src/jexer/package-info.java b/package-info.java similarity index 100% rename from src/jexer/package-info.java rename to package-info.java diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 17ca174..0000000 --- a/pom.xml +++ /dev/null @@ -1,216 +0,0 @@ - - 4.0.0 - com.gitlab.klamonte - jexer - jar - Jexer - Java Text User Interface library that resembles Turbo Vision - 1.0.0-SNAPSHOT - https://gitlab.com/klamonte/jexer - - - - MIT License - http://www.opensource.org/licenses/mit-license.php - repo - - - - - UTF-8 - UTF-8 - - - - scm:git:https://gitlab.com/klamonte/jexer.git - scm:git:https://gitlab.com/klamonte/jexer.git - https://gitlab.com/klamonte/jexer - HEAD - - - - gitlab - https://gitlab.com/klamonte/jexer/issues - - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - ${project.basedir}/src - - - ${project.basedir}/resources - false - - **/* - - - - src - - **/*.java - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - 1.6 - 1.6 - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.0.2 - - - - - jexer.demos.Demo1 - - - - - ${project.version} - - - - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - verify - - jar-no-fork - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - - attach-javadocs - - jar - - - - - - - org.apache.maven.plugins - maven-release-plugin - 2.5.3 - - true - false - forked-path - - - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 - true - - ossrh - https://oss.sonatype.org/ - true - - - - - - - - release-sign-artifacts - - - performRelease - true - - - - - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - sign-artifacts - verify - - sign - - - - - - - - - - - - klamonte - Kevin Lamonte - kevin.lamonte@gmail.com - - - diff --git a/resources/help.xml b/resources/help.xml new file mode 100644 index 0000000..7c68c02 --- /dev/null +++ b/resources/help.xml @@ -0,0 +1,60 @@ + + + + Jexer Help File + Kevin Lamonte + 1.0.0 + Jan 1, 2020 + + + + This [window](Windows) does not have a specific help topic. + See [here](Help On Help) for general information on using the + help system. + + + + + The #{help} system... + + + + + + + #{Menus} do ... + + + + + + #{Windows} do ... + + + + + + The #{text editing} [window](Windows)... + + + + + + + + + + The terminal window ... + + + + + + Copyright (C) 2019 Kevin Lamonte + + Available to all under the MIT License. + + + + + diff --git a/screenshots/jexer_sixel_in_sixel.png b/screenshots/jexer_sixel_in_sixel.png deleted file mode 100644 index 3b3c35d..0000000 Binary files a/screenshots/jexer_sixel_in_sixel.png and /dev/null differ diff --git a/screenshots/new_demo1.png b/screenshots/new_demo1.png deleted file mode 100644 index 675afeb..0000000 Binary files a/screenshots/new_demo1.png and /dev/null differ diff --git a/screenshots/readme_application.png b/screenshots/readme_application.png deleted file mode 100644 index 6e3eed8..0000000 Binary files a/screenshots/readme_application.png and /dev/null differ diff --git a/screenshots/screenshot1.png b/screenshots/screenshot1_old.png similarity index 100% rename from screenshots/screenshot1.png rename to screenshots/screenshot1_old.png diff --git a/screenshots/sixel_color_wheel.png b/screenshots/sixel_color_wheel.png deleted file mode 100644 index d4ce5f7..0000000 Binary files a/screenshots/sixel_color_wheel.png and /dev/null differ diff --git a/screenshots/sixel_images.png b/screenshots/sixel_images.png deleted file mode 100644 index c66e56e..0000000 Binary files a/screenshots/sixel_images.png and /dev/null differ diff --git a/screenshots/snake_swing.png b/screenshots/snake_swing.png deleted file mode 100644 index 85871f9..0000000 Binary files a/screenshots/snake_swing.png and /dev/null differ diff --git a/screenshots/snake_xterm.png b/screenshots/snake_xterm.png deleted file mode 100644 index 4c74874..0000000 Binary files a/screenshots/snake_xterm.png and /dev/null differ diff --git a/screenshots/yodawg.png b/screenshots/yodawg.png deleted file mode 100644 index a7d3d31..0000000 Binary files a/screenshots/yodawg.png and /dev/null differ diff --git a/src/jexer/TEditorWidget.java b/src/jexer/TEditorWidget.java deleted file mode 100644 index a694533..0000000 --- a/src/jexer/TEditorWidget.java +++ /dev/null @@ -1,546 +0,0 @@ -/* - * Jexer - Java Text User Interface - * - * The MIT License (MIT) - * - * Copyright (C) 2019 Kevin Lamonte - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - * @author Kevin Lamonte [kevin.lamonte@gmail.com] - * @version 1 - */ -package jexer; - -import java.io.IOException; - -import jexer.bits.CellAttributes; -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.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 { - - // ------------------------------------------------------------------------ - // Constants -------------------------------------------------------------- - // ------------------------------------------------------------------------ - - /** - * The number of lines to scroll on mouse wheel up/down. - */ - private static final int wheelScrollSize = 3; - - // ------------------------------------------------------------------------ - // Variables -------------------------------------------------------------- - // ------------------------------------------------------------------------ - - /** - * The document being edited. - */ - private Document document; - - /** - * The default color for the TEditor class. - */ - private CellAttributes defaultColor = null; - - /** - * The topmost line number in the visible area. 0-based. - */ - private int topLine = 0; - - /** - * The leftmost column number in the visible area. 0-based. - */ - private int leftColumn = 0; - - // ------------------------------------------------------------------------ - // Constructors ----------------------------------------------------------- - // ------------------------------------------------------------------------ - - /** - * Public constructor. - * - * @param parent parent widget - * @param text text on the screen - * @param x column relative to parent - * @param y row relative to parent - * @param width width of text area - * @param height height of text area - */ - public TEditorWidget(final TWidget parent, final String text, final int x, - final int y, final int width, final int height) { - - // Set parent and window - super(parent, x, y, width, height); - - setCursorVisible(true); - - defaultColor = getTheme().getColor("teditor"); - document = new Document(text, defaultColor); - } - - // ------------------------------------------------------------------------ - // TWidget ---------------------------------------------------------------- - // ------------------------------------------------------------------------ - - /** - * 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. - * - * @param mouse mouse button press event - */ - @Override - public void onMouseDown(final TMouseEvent mouse) { - if (mouse.isMouseWheelUp()) { - for (int i = 0; i < wheelScrollSize; i++) { - if (topLine > 0) { - topLine--; - alignDocument(false); - } - } - return; - } - if (mouse.isMouseWheelDown()) { - for (int i = 0; i < wheelScrollSize; i++) { - if (topLine < document.getLineCount() - 1) { - topLine++; - alignDocument(true); - } - } - return; - } - - if (mouse.isMouse1()) { - // Set the row and column - int newLine = topLine + mouse.getY(); - int newX = leftColumn + mouse.getX(); - 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(); - return; - } - - document.setLineNumber(newLine); - setCursorY(mouse.getY()); - if (newX >= document.getCurrentLine().getDisplayLength()) { - document.end(); - alignCursor(); - } else { - document.setCursor(newX); - setCursorX(mouse.getX()); - } - return; - } - - // Pass to children - super.onMouseDown(mouse); - } - - /** - * Handle keystrokes. - * - * @param keypress keystroke event - */ - @Override - public void onKeypress(final TKeypressEvent keypress) { - if (keypress.equals(kbLeft)) { - document.left(); - alignTopLine(false); - } else if (keypress.equals(kbRight)) { - document.right(); - alignTopLine(true); - } else if (keypress.equals(kbAltLeft) - || keypress.equals(kbCtrlLeft) - ) { - document.backwardsWord(); - alignTopLine(false); - } else if (keypress.equals(kbAltRight) - || keypress.equals(kbCtrlRight) - ) { - document.forwardsWord(); - alignTopLine(true); - } else if (keypress.equals(kbUp)) { - document.up(); - alignTopLine(false); - } else if (keypress.equals(kbDown)) { - document.down(); - alignTopLine(true); - } else if (keypress.equals(kbPgUp)) { - document.up(getHeight() - 1); - alignTopLine(false); - } else if (keypress.equals(kbPgDn)) { - document.down(getHeight() - 1); - alignTopLine(true); - } else if (keypress.equals(kbHome)) { - if (document.home()) { - leftColumn = 0; - if (leftColumn < 0) { - leftColumn = 0; - } - setCursorX(0); - } - } else if (keypress.equals(kbEnd)) { - if (document.end()) { - alignCursor(); - } - } else if (keypress.equals(kbCtrlHome)) { - document.setLineNumber(0); - document.home(); - topLine = 0; - leftColumn = 0; - setCursorX(0); - setCursorY(0); - } else if (keypress.equals(kbCtrlEnd)) { - document.setLineNumber(document.getLineCount() - 1); - document.end(); - alignTopLine(false); - } else if (keypress.equals(kbIns)) { - document.setOverwrite(!document.getOverwrite()); - } else if (keypress.equals(kbDel)) { - 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(' '); - } - alignCursor(); - } else if (keypress.equals(kbEnter)) { - document.enter(); - alignTopLine(true); - } else if (!keypress.getKey().isFnKey() - && !keypress.getKey().isAlt() - && !keypress.getKey().isCtrl() - ) { - // Plain old keystroke, process it - document.addChar(keypress.getKey().getChar()); - alignCursor(); - } else { - // Pass other keys (tab etc.) on to TWidget - super.onKeypress(keypress); - } - } - - /** - * Method that subclasses can override to handle window/screen resize - * events. - * - * @param resize resize event - */ - @Override - public void onResize(final TResizeEvent resize) { - // Change my width/height, and pull the cursor in as needed. - if (resize.getType() == TResizeEvent.Type.WIDGET) { - setWidth(resize.getWidth()); - setHeight(resize.getHeight()); - // See if the cursor is now outside the window, and if so move - // things. - if (getCursorX() >= getWidth()) { - leftColumn += getCursorX() - (getWidth() - 1); - setCursorX(getWidth() - 1); - } - if (getCursorY() >= getHeight()) { - topLine += getCursorY() - (getHeight() - 1); - setCursorY(getHeight() - 1); - } - } else { - // Let superclass handle it - super.onResize(resize); - } - } - - // ------------------------------------------------------------------------ - // TEditorWidget ---------------------------------------------------------- - // ------------------------------------------------------------------------ - - /** - * Align visible area with document current line. - * - * @param topLineIsTop if true, make the top visible line the document - * current line if it was off-screen. If false, make the bottom visible - * line the document current line. - */ - private void alignTopLine(final boolean topLineIsTop) { - int line = document.getLineNumber(); - - if ((line < topLine) || (line > topLine + getHeight() - 1)) { - // Need to move topLine to bring document back into view. - if (topLineIsTop) { - topLine = line - (getHeight() - 1); - if (topLine < 0) { - topLine = 0; - } - assert (topLine >= 0); - } else { - topLine = line; - assert (topLine >= 0); - } - } - - /* - System.err.println("line " + line + " topLine " + topLine); - */ - - // Document is in view, let's set cursorY - assert (line >= topLine); - setCursorY(line - topLine); - alignCursor(); - } - - /** - * Align document current line with visible area. - * - * @param topLineIsTop if true, make the top visible line the document - * current line if it was off-screen. If false, make the bottom visible - * line the document current line. - */ - private void alignDocument(final boolean topLineIsTop) { - int line = document.getLineNumber(); - int cursor = document.getCursor(); - - if ((line < topLine) || (line > topLine + getHeight() - 1)) { - // Need to move document to ensure it fits view. - if (topLineIsTop) { - document.setLineNumber(topLine); - } else { - document.setLineNumber(topLine + (getHeight() - 1)); - } - if (cursor < document.getCurrentLine().getDisplayLength()) { - document.setCursor(cursor); - } - } - - /* - System.err.println("getLineNumber() " + document.getLineNumber() + - " topLine " + topLine); - */ - - // Document is in view, let's set cursorY - setCursorY(document.getLineNumber() - topLine); - alignCursor(); - } - - /** - * Align visible cursor with document cursor. - */ - private void alignCursor() { - int width = getWidth(); - - int desiredX = document.getCursor() - leftColumn; - if (desiredX < 0) { - // We need to push the screen to the left. - leftColumn = document.getCursor(); - } else if (desiredX > width - 1) { - // We need to push the screen to the right. - leftColumn = document.getCursor() - (width - 1); - } - - /* - System.err.println("document cursor " + document.getCursor() + - " leftColumn " + leftColumn); - */ - - - setCursorX(document.getCursor() - leftColumn); - } - - /** - * Get the number of lines in the underlying Document. - * - * @return the number of lines - */ - public int getLineCount() { - return document.getLineCount(); - } - - /** - * Get the current visible top row number. 1-based. - * - * @return the visible top row number. Row 1 is the first row. - */ - public int getVisibleRowNumber() { - return topLine + 1; - } - - /** - * Set the current visible row number. 1-based. - * - * @param row the new visible row number. Row 1 is the first row. - */ - public void setVisibleRowNumber(final int row) { - assert (row > 0); - if ((row > 0) && (row < document.getLineCount())) { - topLine = row - 1; - alignDocument(true); - } - } - - /** - * Get the current editing row number. 1-based. - * - * @return the editing row number. Row 1 is the first row. - */ - public int getEditingRowNumber() { - return document.getLineNumber() + 1; - } - - /** - * Set the current editing row number. 1-based. - * - * @param row the new editing row number. Row 1 is the first row. - */ - public void setEditingRowNumber(final int row) { - assert (row > 0); - if ((row > 0) && (row < document.getLineCount())) { - document.setLineNumber(row - 1); - alignTopLine(true); - } - } - - /** - * Set the current visible column number. 1-based. - * - * @return the visible column number. Column 1 is the first column. - */ - public int getVisibleColumnNumber() { - return leftColumn + 1; - } - - /** - * Set the current visible column number. 1-based. - * - * @param column the new visible column number. Column 1 is the first - * column. - */ - public void setVisibleColumnNumber(final int column) { - assert (column > 0); - if ((column > 0) && (column < document.getLineLengthMax())) { - leftColumn = column - 1; - alignDocument(true); - } - } - - /** - * Get the current editing column number. 1-based. - * - * @return the editing column number. Column 1 is the first column. - */ - public int getEditingColumnNumber() { - return document.getCursor() + 1; - } - - /** - * Set the current editing column number. 1-based. - * - * @param column the new editing column number. Column 1 is the first - * column. - */ - public void setEditingColumnNumber(final int column) { - if ((column > 0) && (column < document.getLineLength())) { - document.setCursor(column - 1); - alignCursor(); - } - } - - /** - * Get the maximum possible row number. 1-based. - * - * @return the maximum row number. Row 1 is the first row. - */ - public int getMaximumRowNumber() { - return document.getLineCount() + 1; - } - - /** - * Get the maximum possible column number. 1-based. - * - * @return the maximum column number. Column 1 is the first column. - */ - public int getMaximumColumnNumber() { - return document.getLineLengthMax() + 1; - } - - /** - * Get the dirty value. - * - * @return true if the buffer is dirty - */ - public boolean isDirty() { - return document.isDirty(); - } - - /** - * Save contents to file. - * - * @param filename file to save to - * @throws IOException if a java.io operation throws - */ - public void saveToFilename(final String filename) throws IOException { - document.saveToFilename(filename); - } - -} diff --git a/src/jexer/teditor/Document.java b/teditor/Document.java similarity index 77% rename from src/jexer/teditor/Document.java rename to teditor/Document.java index 2abfef6..b4a9a3b 100644 --- a/src/jexer/teditor/Document.java +++ b/teditor/Document.java @@ -76,6 +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 @@ public class Document { 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 @@ public class Document { } } + /** + * 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 @@ public class Document { return dirty; } + /** + * Unset the dirty flag. + */ + public void setNotDirty() { + dirty = false; + } + /** * Save contents to file. * @@ -133,7 +183,11 @@ public class Document { "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 @@ public class Document { // 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 @@ public class Document { // 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 @@ public class Document { 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 @@ public class Document { } 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 @@ public class Document { } 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 @@ public class Document { } 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 @@ public class Document { } 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 @@ public class Document { } 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 @@ public class Document { 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 @@ public class Document { } } + /** + * 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 @@ public class Document { 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 --git a/src/jexer/teditor/Highlighter.java b/teditor/Highlighter.java similarity index 76% rename from src/jexer/teditor/Highlighter.java rename to teditor/Highlighter.java index a484194..23ee900 100644 --- a/src/jexer/teditor/Highlighter.java +++ b/teditor/Highlighter.java @@ -56,13 +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 @@ public class Highlighter { * @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 @@ public class Highlighter { * 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 --git a/src/jexer/teditor/Line.java b/teditor/Line.java similarity index 70% rename from src/jexer/teditor/Line.java rename to teditor/Line.java index 7cd5feb..b5c980a 100644 --- a/src/jexer/teditor/Line.java +++ b/teditor/Line.java @@ -32,6 +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 @@ 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 @@ public class Line { 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 @@ public class Line { } /** - * 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 @@ public class Line { if (getDisplayLength() == 0) { return false; } - if (position == getDisplayLength() - 1) { + if (screenPosition == getDisplayLength() - 1) { return false; } if (position < rawText.length()) { @@ -267,7 +325,7 @@ public class Line { * @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 @@ public class Line { 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 @@ public class Line { /** * 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 @@ public class Line { * @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 @@ public class Line { * @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 @@ public class Line { * @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 @@ public class Line { " 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 --git a/src/jexer/teditor/Word.java b/teditor/Word.java similarity index 96% rename from src/jexer/teditor/Word.java rename to teditor/Word.java index eada29c..483f9c3 100644 --- a/src/jexer/teditor/Word.java +++ b/teditor/Word.java @@ -135,11 +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 --git a/src/jexer/teditor/package-info.java b/teditor/package-info.java similarity index 100% rename from src/jexer/teditor/package-info.java rename to teditor/package-info.java diff --git a/src/jexer/tterminal/DECCharacterSets.java b/tterminal/DECCharacterSets.java similarity index 100% rename from src/jexer/tterminal/DECCharacterSets.java rename to tterminal/DECCharacterSets.java diff --git a/src/jexer/tterminal/DisplayLine.java b/tterminal/DisplayLine.java similarity index 92% rename from src/jexer/tterminal/DisplayLine.java rename to tterminal/DisplayLine.java index 06a05a3..87e6952 100644 --- a/src/jexer/tterminal/DisplayLine.java +++ b/tterminal/DisplayLine.java @@ -248,4 +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 --git a/src/jexer/tterminal/DisplayListener.java b/tterminal/DisplayListener.java similarity index 100% rename from src/jexer/tterminal/DisplayListener.java rename to tterminal/DisplayListener.java diff --git a/src/jexer/tterminal/ECMA48.java b/tterminal/ECMA48.java similarity index 91% rename from src/jexer/tterminal/ECMA48.java rename to tterminal/ECMA48.java index 1d34811..537b2e0 100644 --- a/src/jexer/tterminal/ECMA48.java +++ b/tterminal/ECMA48.java @@ -28,9 +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 @@ 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 @@ public class ECMA48 implements Runnable { /** * The type of emulator to be. */ - private DeviceType type = DeviceType.VT102; + private final DeviceType type; /** * The scrollback buffer characters + attributes. @@ -271,7 +274,7 @@ public class ECMA48 implements Runnable { /** * 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 @@ public class ECMA48 implements Runnable { * 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 @@ public class ECMA48 implements Runnable { * 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 @@ public class ECMA48 implements Runnable { /** * Non-csi collect buffer. */ - private StringBuilder collectBuffer; + private StringBuilder collectBuffer = new StringBuilder(128); /** * When true, use the G1 character set. @@ -469,7 +472,7 @@ public class ECMA48 implements Runnable { /** * Sixel collection buffer. */ - private StringBuilder sixelParseBuffer; + private StringBuilder sixelParseBuffer = new StringBuilder(2048); /** * Sixel shared palette. @@ -503,6 +506,11 @@ public class ECMA48 implements Runnable { */ 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 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { // 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 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { // 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 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { */ private void toGround() { csiParams.clear(); - collectBuffer = new StringBuilder(8); + collectBuffer.setLength(0); scanState = ScanState.GROUND; } @@ -1265,7 +1335,7 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { * Handle a linefeed. */ private void linefeed() { - if (currentState.cursorY < scrollRegionBottom) { // Increment screen y currentState.cursorY++; @@ -1664,35 +1987,45 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { if (decPrivateModeFlag == true) { if (value == true) { // Enable sixel scrolling (default). - // TODO + // Not supported } else { // Disable sixel scrolling. - // TODO + // Not supported } } } @@ -3939,14 +4283,14 @@ public class ECMA48 implements Runnable { * RGB color mode. */ rgbColor = true; - break; + continue; case 5: /* * Indexed color mode. */ idx88Color = true; - break; + continue; default: /* @@ -3994,7 +4338,7 @@ public class ECMA48 implements Runnable { case 8: // Invisible - // TODO + // Not supported break; case 90: @@ -4379,6 +4723,9 @@ public class ECMA48 implements Runnable { // 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 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { } } - 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 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { * * @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 @@ public class ECMA48 implements Runnable { // 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 @@ public class ECMA48 implements Runnable { // 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 @@ public class ECMA48 implements Runnable { // 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 @@ public class ECMA48 implements Runnable { * @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 @@ public class ECMA48 implements Runnable { 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 @@ public class ECMA48 implements Runnable { } } + 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 @@ public class ECMA48 implements Runnable { } 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 @@ public class ECMA48 implements Runnable { // 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 --git a/src/jexer/tterminal/Sixel.java b/tterminal/Sixel.java similarity index 99% rename from src/jexer/tterminal/Sixel.java rename to tterminal/Sixel.java index a4c00fc..b91e77a 100644 --- a/src/jexer/tterminal/Sixel.java +++ b/tterminal/Sixel.java @@ -31,7 +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 @@ 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 --git a/src/jexer/tterminal/package-info.java b/tterminal/package-info.java similarity index 100% rename from src/jexer/tterminal/package-info.java rename to tterminal/package-info.java diff --git a/src/jexer/ttree/TDirectoryTreeItem.java b/ttree/TDirectoryTreeItem.java similarity index 100% rename from src/jexer/ttree/TDirectoryTreeItem.java rename to ttree/TDirectoryTreeItem.java diff --git a/src/jexer/ttree/TTreeItem.java b/ttree/TTreeItem.java similarity index 100% rename from src/jexer/ttree/TTreeItem.java rename to ttree/TTreeItem.java diff --git a/src/jexer/ttree/TTreeView.java b/ttree/TTreeView.java similarity index 100% rename from src/jexer/ttree/TTreeView.java rename to ttree/TTreeView.java diff --git a/src/jexer/ttree/TTreeViewWidget.java b/ttree/TTreeViewWidget.java similarity index 92% rename from src/jexer/ttree/TTreeViewWidget.java rename to ttree/TTreeViewWidget.java index 080a200..13beac3 100644 --- a/src/jexer/ttree/TTreeViewWidget.java +++ b/ttree/TTreeViewWidget.java @@ -268,11 +268,55 @@ public class TTreeViewWidget extends TScrollableWidget { // 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; diff --git a/src/jexer/ttree/TTreeViewWindow.java b/ttree/TTreeViewWindow.java similarity index 100% rename from src/jexer/ttree/TTreeViewWindow.java rename to ttree/TTreeViewWindow.java diff --git a/src/jexer/ttree/package-info.java b/ttree/package-info.java similarity index 100% rename from src/jexer/ttree/package-info.java rename to ttree/package-info.java