--- /dev/null
+/*
+ * 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();
+
+}
+++ /dev/null
-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.
+++ /dev/null
-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
-<dependency>
- <groupId>com.gitlab.klamonte</groupId>
- <artifactId>jexer</artifactId>
- <version>0.3.2</version>
-</dependency>
-```
-
-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/) .
package jexer;
import java.io.File;
+import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
import jexer.bits.ColorTheme;
import jexer.bits.StringUtils;
import jexer.event.TCommandEvent;
import jexer.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;
*/
private Backend backend;
+ /**
+ * The clipboard for copy and paste.
+ */
+ private Clipboard clipboard = new Clipboard();
+
/**
* Actual mouse coordinate X.
*/
*/
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.
*/
*/
private List<TWindow> windows;
- /**
- * The currently acive window.
- */
- private TWindow activeWindow = null;
-
/**
* Timers that are being ticked.
*/
*/
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<Topic> helpTopics = new ArrayList<Topic>();
+
/**
* WidgetEventHandler is the main event consumer loop. There are at most
* two such threads in existence: the primary for normal case and a
}
}
+ // 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);
+ }
+ }
+ });
}
// ------------------------------------------------------------------------
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;
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<String> filters = new ArrayList<String>();
+ filters.add("^.*\\.[Xx][Mm][Ll]$");
+ String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN,
+ filters);
+ if (filename != null) {
+ helpTopics = new ArrayList<Topic>();
+ 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;
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;
}
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();
}
mouseX = 0;
mouseY = 0;
- oldMouseX = 0;
- oldMouseY = 0;
}
if (desktop != null) {
desktop.setDimensions(0, desktopTop, resize.getWidth(),
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 {
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.
// 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())) {
// Dispatch events to the active window -------------------------------
boolean dispatchToDesktop = true;
- TWindow window = activeWindow;
+ TWindow window = getActiveWindow();
if (window != null) {
assert (window.isActive());
assert (window.isShown());
TMouseEvent mouse = (TMouseEvent) event;
if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
- oldMouseX = mouseX;
- oldMouseY = mouseY;
mouseX = mouse.getX();
mouseY = mouse.getY();
} else {
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.
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<Runnable> invokes = new ArrayList<Runnable>();
synchronized (invokeLaters) {
- for (Runnable invoke: invokeLaters) {
- invoke.run();
- }
+ invokes.addAll(invokeLaters);
invokeLaters.clear();
}
+ for (Runnable invoke: invokes) {
+ invoke.run();
+ }
+ doRepaint();
}
return theme;
}
+ /**
+ * Get the clipboard.
+ *
+ * @return the clipboard
+ */
+ public final Clipboard getClipboard() {
+ return clipboard;
+ }
+
/**
* Repaint the screen on the next update.
*/
* @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;
}
/**
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),
// ------------------------------------------------------------------------
/**
- * 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) {
}
}
- 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);
}
/**
}
}
+ 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;
// 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();
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;
*
* @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
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;
}
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)
+
}
/**
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
*/
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++;
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();
synchronized (secondaryEventHandler) {
secondaryEventHandler.notify();
}
- }
+
+ } // synchronized (windows)
// Permit desktop to be active if it is the only thing left.
if (desktop != null) {
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)
}
/**
}
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)
if (desktop != null) {
desktop.setActive(false);
}
+
}
/**
* @return true if the active window is overriding the menu
*/
private boolean overrideMenuWindowActive() {
+ TWindow activeWindow = getActiveWindow();
if (activeWindow != null) {
if (activeWindow.hasOverriddenMenu()) {
return true;
|| (mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
) {
synchronized (windows) {
- Collections.sort(windows);
if (windows.get(0).isModal()) {
// Modal windows don't switch
return;
}
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;
}
}
*/
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"));
aboutDialogTitle=About
aboutDialogText=Jexer Version {0}
+
+searchHelpInputBoxTitle=Search Help Topics
+searchHelpInputBoxCaption=Search help topics for (regex):
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 ---------------------------------------------------------
--- /dev/null
+/*
+ * 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<SavedState> undoList = new ArrayList<SavedState>();
+
+ /**
+ * 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);
+ }
+
+}
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.*;
}
// ------------------------------------------------------------------------
- // 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);
}
/**
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.
final Throwable exception) {
super(application, i18n.getString("windowTitle"),
- 1, 1, 70, 20, CENTERED | MODAL);
+ 1, 1, 78, 22, CENTERED | MODAL);
this.exception = exception;
2, 6, "ttext", false);
ArrayList<String> stackTraceStrings = new ArrayList<String>();
+ 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();
});
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.
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}
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 --------------------------------------------------------------
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 +
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 ----------------------------------------------------------------
// ------------------------------------------------------------------------
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());
+ }
}
/**
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;
+ }
+
}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+windowTitle=Help
+previousButton=Pre&vious
+contentsButton=Co&ntents
+indexButton=\ &Index\ \
+closeButton=\ C&lose\ \
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 --------------------------------------------------------------
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 ----------------------------------------------------------------
// ------------------------------------------------------------------------
}
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;
}
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;
+ }
+
}
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.
*/
return "\u25C0\u2500\u2518";
}
+ // Special case: Space is "Space"
+ if (equals(kbSpace)) {
+ return "Space";
+ }
+
if (equals(kbShiftLeft)) {
return "Shift+\u2190";
}
@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);
+ }
}
/**
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 {
package jexer;
import jexer.bits.CellAttributes;
-import jexer.bits.GraphicsChars;
import jexer.bits.StringUtils;
/**
// ------------------------------------------------------------------------
/**
- * 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.
/**
* 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
* @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
setCursorVisible(true);
setCursorX(1);
+
+ parent.addRadioButton(this);
}
// ------------------------------------------------------------------------
public void onMouseDown(final TMouseEvent mouse) {
if ((mouseOnRadioButton(mouse)) && (mouse.isMouse1())) {
// Switch state
- selected = true;
- ((TRadioGroup) getParent()).setSelected(this);
+ ((TRadioGroup) getParent()).setSelected(id);
}
}
public void onKeypress(final TKeypressEvent keypress) {
if (keypress.equals(kbSpace)) {
- selected = true;
- ((TRadioGroup) getParent()).setSelected(this);
+ ((TRadioGroup) getParent()).setSelected(id);
return;
}
}
/**
- * 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);
+ }
}
/**
* 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.
*
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.
*
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.
*
* @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);
// 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);
+ }
+ }
}
}
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
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)
}
} 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)
keep.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
getHeight()));
}
-
+
return keep;
}
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) {
*/
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);
*/
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);
*/
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;
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.
*/
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
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
+ /**
+ * Static constructor.
+ */
+ static {
+ checkForPtypipe();
+ }
+
/**
* Public constructor spawns a custom command line.
*
* @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) {
* @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,
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";
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);
}
* @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) {
* @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) {
// 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.
) {
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+"));
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 ------------------------------------------------------
// ------------------------------------------------------------------------
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)) {
// 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.
*
}
});
}
- if (getApplication() != null) {
- getApplication().postEvent(new TMenuEvent(
- TMenu.MID_REPAINT));
- }
+ app.doRepaint();
}
}
} // 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.
* 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));
}
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;
+ }
+
}
*/
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.*;
/**
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();
}
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() {
*/
@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);
}
}
+ /**
+ * Get this window's help topic to load.
+ *
+ * @return the topic name
+ */
+ @Override
+ public String getHelpTopic() {
+ return "Terminal Window";
+ }
+
// ------------------------------------------------------------------------
// TTerminalWindow --------------------------------------------------------
// ------------------------------------------------------------------------
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();
+ }
+
}
windowTitle=Terminal
statusBarRunning=Terminal session executing...
+statusBarHelp=Help
+statusBarMenu=Menu
package jexer;
import java.util.Arrays;
-import java.util.LinkedList;
+import java.util.ArrayList;
import java.util.List;
import jexer.bits.CellAttributes;
this.text = text;
this.colorKey = colorKey;
- lines = new LinkedList<String>();
+ lines = new ArrayList<String>();
vScroller = new TVScroller(this, getWidth() - 1, 0,
Math.max(1, getHeight() - 1));
/**
* 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;
reflowData();
}
+ /**
+ * Un-justify the text.
+ */
+ public void unJustify() {
+ justification = Justification.NONE;
+ reflowData();
+ }
+
}
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;
* @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);
}
}
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:
* <ul>
* 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)
) {
*
* @param child TWidget to add
*/
- private void addChild(final TWidget child) {
+ public void addChild(final TWidget child) {
children.add(child);
if ((child.enabled)
* @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;
}
return false;
+>>>>>>> upstream-sep2019-tcombo:src/jexer/TWidget.java
}
/**
if (activeChild != null) {
activeChild.active = false;
}
- child.active = true;
- activeChild = child;
}
+ child.active = true;
+ activeChild = child;
}
}
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.
*
*/
private boolean hideMouse = false;
+ /**
+ * The help topic for this window.
+ */
+ protected String helpTopic = "Help";
+
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
}
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));
// 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
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.
*
*/
@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());
}
*/
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;
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;
MOUSE_SGR,
}
+ /**
+ * Available Jexer images support.
+ */
+ private enum JexerImageOption {
+ DISABLED,
+ JPG,
+ PNG,
+ RGB,
+ }
+
// ------------------------------------------------------------------------
// Variables --------------------------------------------------------------
// ------------------------------------------------------------------------
*/
private boolean sixel = true;
+ /**
+ * If true, use a single shared palette for sixel.
+ */
+ private boolean sixelSharedPalette = true;
+
/**
* The sixel palette handler.
*/
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.
*/
// 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(),
// 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(),
// 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;
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();
}
* @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;
}
/**
* @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;
}
/**
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));
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;
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);
}
/**
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;
// 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);
}
/**
if (decPrivateModeFlag == false) {
break;
}
+ boolean reportsJexerImages = false;
+ boolean reportsIterm2Images = false;
for (String x: params) {
if (x.equals("4")) {
// Terminal reports sixel support
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
* - 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";
+ }
}
/**
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();
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++) {
// 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);
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) {
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<Cell> 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<Cell> 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();
}
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.
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;
}
}
+ 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<Cell> 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:
*
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;",
getTextHeight())));
*/
sb.append("inline=1:");
- sb.append(base64.encodeToString(pngOutputStream.toByteArray()));
+ sb.append(StringUtils.toBase64(pngOutputStream.toByteArray()));
sb.append("\007");
if (saveInCache) {
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++) {
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
// 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.
* @return true if this terminal is emitting Jexer images
*/
public boolean hasJexerImages() {
- return jexerImages;
+ return (jexerImageOption != JexerImageOption.DISABLED);
}
// ------------------------------------------------------------------------
package jexer.backend;
import java.awt.Font;
+import java.awt.FontFormatException;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
if (filename.length() == 0) {
// Fallback font
- font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2);
return;
}
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);
}
}
--- /dev/null
+/*
+ * 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<TInputEvent> 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
+ }
+
+}
import jexer.backend.GlyphMaker;
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
import jexer.bits.GraphicsChars;
import jexer.bits.StringUtils;
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());
+ }
+
}
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
/**
* MultiScreen mirrors its I/O to several screens.
* @return drawing boundary
*/
public int getClipRight() {
- return screens.get(0).getClipRight();
+ if (screens.size() > 0) {
+ return screens.get(0).getClipRight();
+ }
+ return 0;
}
/**
* @return drawing boundary
*/
public int getClipBottom() {
- return screens.get(0).getClipBottom();
+ if (screens.size() > 0) {
+ return screens.get(0).getClipBottom();
+ }
+ return 0;
}
/**
* @return drawing boundary
*/
public int getClipLeft() {
- return screens.get(0).getClipLeft();
+ if (screens.size() > 0) {
+ return screens.get(0).getClipLeft();
+ }
+ return 0;
}
/**
* @return drawing boundary
*/
public int getClipTop() {
- return screens.get(0).getClipTop();
+ if (screens.size() > 0) {
+ return screens.get(0).getClipTop();
+ }
+ return 0;
}
/**
* @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();
}
/**
* @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();
}
/**
*/
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();
*/
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();
* @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;
}
/**
* @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;
}
/**
* @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;
}
/**
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);
+ }
+ }
+
}
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
/**
* Drawing operations API.
*/
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);
+
}
* 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 -----------------------------------------------------------
*/
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();
}
*/
public SwingComponent(final JComponent component) {
this.component = component;
+ adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER);
setupComponent();
}
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;
) {
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();
// 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;
}
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);
} 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);
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;
}
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;
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);
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);
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;
}
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;
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);
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;
}
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;
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);
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;
}
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;
}
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);
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) {
/*
*/
private static final int PROTECT = 0x10;
-
// ------------------------------------------------------------------------
// Variables --------------------------------------------------------------
// ------------------------------------------------------------------------
--- /dev/null
+/*
+ * 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;
+ }
+
+}
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();
}
// 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.
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();
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);
+
}
/**
import java.util.List;
import java.util.ArrayList;
+import java.util.Arrays;
/**
* StringUtils contains methods to:
*
* - 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 {
* @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);
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.<br><br> 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
+ * <code>sun.misc.Encoder()/Decoder()</code>.<br><br>
+ *
+ * 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 <code>String</code>
+ * this version is about three times as fast due to the fact that the
+ * Commons Codec result has to be recoded to a <code>String</code> from
+ * <code>byte[]</code>, which is very expensive.<br><br>
+ *
+ * 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
+ * <code>sun.misc.Encoder()/Decoder()</code> produce temporary arrays but
+ * since performance is quite low it probably does.<br><br>
+ *
+ * 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.<br> Commons codec seem to always att a trailing line
+ * separator.<br><br>
+ *
+ * <b>Note!</b> The encode/decode method pairs (types) come in three
+ * versions with the <b>exact</b> 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.<br><br>
+ *
+ * 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.<br><br>
+ *
+ * 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 <code>byte[]</code>
+ * representation i accordance with RFC 2045.
+ * @param sArr The bytes to convert. If <code>null</code> or length 0
+ * an empty array will be returned.
+ * @return A BASE64 encoded array. Never <code>null</code>.
+ */
+ 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. <code>null</code> will throw an exception.
+ * @return The decoded array of bytes. May be of length 0. Will be
+ * <code>null</code> 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;
+ }
+
}
+++ /dev/null
-<!--
-
- Jexer - Java Text User Interface - Ant build
-
- 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.
-
--->
-
-<project name="jexer" basedir="." default="jar">
-
- <property name="version" value="0.3.2"/>
- <property name="src.dir" value="src"/>
- <property name="resources.dir" value="resources"/>
- <property name="build.dir" value="build"/>
- <property name="classes.dir" value="${build.dir}/classes"/>
- <property name="jar.dir" value="${build.dir}/jar"/>
- <property name="apidocs.dir" value="docs/api"/>
-
- <target name="clean">
- <delete dir="${build.dir}"/>
- <delete dir="${apidocs.dir}"/>
- </target>
-
- <target name="compile">
- <mkdir dir="${classes.dir}"/>
- <javac srcdir="${src.dir}" destdir="${classes.dir}"
- includeantruntime="false"
- debug="on"
- debuglevel="lines,vars,source"
- target="1.6"
- source="1.6"
- />
- </target>
-
- <target name="jar" depends="compile">
- <mkdir dir="${jar.dir}"/>
- <jar destfile="${jar.dir}/${ant.project.name}.jar"
- basedir="${classes.dir}">
- <fileset dir="${resources.dir}"/>
-
- <!-- Include properties files. -->
- <fileset dir="${src.dir}" includes="**/*.properties"/>
-
- <!-- Include source by default. -->
- <!-- <fileset dir="${src.dir}"/> -->
-
- <manifest>
- <attribute name="Main-Class" value="jexer.demos.Demo1"/>
- <attribute name="Implementation-Version" value="${version}"/>
- </manifest>
- </jar>
- </target>
-
- <target name="run" depends="jar">
- <java jar="${jar.dir}/${ant.project.name}.jar" fork="true">
- <arg value="-Djexer.Swing=true"/>
- </java>
- </target>
-
- <target name="clean-build" depends="clean,jar"/>
-
- <target name="build" depends="jar"/>
-
- <target name="doc" depends="docs"/>
-
- <!--
- For Java 11+, add additionalparam="dash-dash-frames". My
- workflow is back to Java 8, so leaving this comment here for
- myself when Debian stables moves to Java 11.
- -->
-
-<target name="docs" depends="jar">
- <javadoc
- destdir="${apidocs.dir}"
- author="true"
- version="true"
- use="true"
- access="protected"
- windowtitle="Jexer - Java Text User Interface - API docs"
- >
- <fileset dir="${src.dir}" defaultexcludes="yes">
- <include name="jexer/**/*.java"/>
- </fileset>
-
- <doctitle>
- <![CDATA[<h1>Jexer - Java Text User Interface Library</h1>]]>
- </doctitle>
- <bottom>
- <![CDATA[<i>Copyright © 2019 Kevin Lamonte. Licensed MIT.</i>]]>
- </bottom>
- <!--
- <tag name="todo" scope="all" description="To do:"/>
- <group title="Group 1 Packages" packages="com.dummy.test.a*"/>
- <group title="Group 2 Packages" packages="com.dummy.test.b*:com.dummy.test.c*"/>
- <link offline="true"
- href="http://docs.oracle.com/javase/7/docs/api/"
- packagelistLoc="C:\tmp"/>
- <link href="http://docs.oracle.com/javase/7/docs/api/"/>
- -->
- </javadoc>
- </target>
-
-</project>
* 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
--- /dev/null
+/*
+ * 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
+ }
+ }
+ }
+ }
+
+}
--- /dev/null
+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...
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<String> comboValues = new ArrayList<String>();
comboValues.add(i18n.getString("comboBoxString0"));
+++ /dev/null
-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.
+++ /dev/null
-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:
+++ /dev/null
-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).
*/
private boolean mouseWheelDown;
+ /**
+ * Keyboard modifier ALT.
+ */
+ private boolean alt;
+
+ /**
+ * Keyboard modifier CTRL.
+ */
+ private boolean ctrl;
+
+ /**
+ * Keyboard modifier SHIFT.
+ */
+ private boolean shift;
+
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
* @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;
this.mouse3 = mouse3;
this.mouseWheelUp = mouseWheelUp;
this.mouseWheelDown = mouseWheelDown;
+ this.alt = alt;
+ this.ctrl = ctrl;
+ this.shift = shift;
}
// ------------------------------------------------------------------------
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.
*
*/
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;
}
*/
@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,
mouse2,
mouse3,
mouseWheelUp,
- mouseWheelDown);
+ mouseWheelDown,
+ alt, ctrl, shift);
}
}
+++ /dev/null
-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();
- }
-}
+++ /dev/null
-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<String> filters = new ArrayList<String>();
- 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<String> 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);
- }
-
-}
+++ /dev/null
-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;
- }
-
-}
+++ /dev/null
-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;
- }
- }
- });
- }
-
-}
+++ /dev/null
-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();
- }
-}
--- /dev/null
+#!/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;
+# <sequence> ST, and for all ESCs in <sequence> 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
+
--- /dev/null
+/*
+ * 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<String, Topic> topicsByTitle;
+
+ /**
+ * The map of topics by index key term.
+ */
+ private HashMap<String, Topic> 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<String, Topic>();
+ topicsByTerm = new HashMap<String, Topic>();
+
+ 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<Topic> allTopics = new ArrayList<Topic>();
+ allTopics.addAll(topicsByTitle.values());
+ Collections.sort(allTopics);
+
+ List<Topic> results = new ArrayList<Topic>();
+ 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<Link> links = new ArrayList<Link>();
+ 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<Topic> allTopics = new ArrayList<Topic>();
+ allTopics.addAll(topicsByTitle.values());
+ Collections.sort(allTopics);
+
+ StringBuilder text = new StringBuilder();
+ int wordIndex = 0;
+ List<Link> links = new ArrayList<Link>();
+ 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<Topic> allTopics = new ArrayList<Topic>();
+ allTopics.addAll(topicsByTitle.values());
+
+ HashMap<String, ArrayList<Topic>> allKeys;
+ allKeys = new HashMap<String, ArrayList<Topic>>();
+ for (Topic topic: allTopics) {
+ for (String key: topic.getIndexKeys()) {
+ key = key.toLowerCase();
+ ArrayList<Topic> topics = allKeys.get(key);
+ if (topics == null) {
+ topics = new ArrayList<Topic>();
+ allKeys.put(key, topics);
+ }
+ topics.add(topic);
+ }
+ }
+ List<String> keys = new ArrayList<String>();
+ keys.addAll(allKeys.keySet());
+ Collections.sort(keys);
+
+ StringBuilder text = new StringBuilder();
+ int wordIndex = 0;
+ List<Link> links = new ArrayList<Link>();
+
+ for (String key: keys) {
+ List<Topic> 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);
+ }
+ }
+
+}
--- /dev/null
+tableOfContents=Table Of Contents
+index=Index
+searchResults=Search Results - {0}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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<TParagraph> 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<TParagraph>();
+
+ // Add title paragraph at top. We explicitly set the separator to
+ // false to achieve the underscore effect.
+ List<TWord> title = new ArrayList<TWord>();
+ title.add(new TWord(topic.getTitle(), null));
+ TParagraph titleParagraph = new TParagraph(this, title);
+ titleParagraph.separator = false;
+ paragraphs.add(titleParagraph);
+ title = new ArrayList<TWord>();
+ 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<TWord> words = new ArrayList<TWord>();
+ 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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<TWord> 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<TWord> 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;
+ }
+
+
+}
--- /dev/null
+/*
+ * 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 ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+}
--- /dev/null
+/*
+ * 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<Topic> {
+
+ /**
+ * 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<String> indexKeys = new HashSet<String>();
+
+ /**
+ * The links in this topic.
+ */
+ private List<Link> links = new ArrayList<Link>();
+
+ // ------------------------------------------------------------------------
+ // 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<Link> 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<String> getIndexKeys() {
+ return indexKeys;
+ }
+
+ /**
+ * Get the links.
+ *
+ * @return the links
+ */
+ public List<Link> 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)
+
+ }
+
+}
--- /dev/null
+topicNotFoundTitle=Topic Not Found
+topicNotFoundText=The help topic was not found.
--- /dev/null
+/*
+ * 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;
if (timeoutMillis == 0) {
// Block on the read().
- return stream.read(b);
+ return stream.read(b, off, len);
}
int remaining = len;
*/
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()) {
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;
*/
private MnemonicString mnemonic;
+ /**
+ * If true, draw icons with menu items. Note package private access.
+ */
+ boolean useIcons = false;
+
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
setHeight(2);
setActive(false);
+
+ if (System.getProperty("jexer.menuIcons", "false").equals("true")) {
+ useIcons = true;
+ }
+
}
// ------------------------------------------------------------------------
final boolean enabled) {
assert (id >= 1024);
- return addItemInternal(id, label, null, enabled);
+ return addItemInternal(id, label, null, enabled, -1);
}
/**
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);
}
/**
* @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);
String label;
TKeypress key = null;
+ int icon = -1;
boolean checkable = false;
boolean checked = false;
case MID_REPAINT:
label = i18n.getString("menuRepaintDesktop");
+ icon = 0x1F3A8;
break;
case MID_VIEW_IMAGE:
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");
break;
case MID_CASCADE:
label = i18n.getString("menuWindowCascade");
+ icon = 0x1F5D7;
break;
case MID_CLOSE_ALL:
label = i18n.getString("menuWindowCloseAll");
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");
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;
}
menuExit=E&xit
menuShell=O&S Shell
menuOpen=&Open
+menuUndo=&Undo
+menuRedo=&Redo
menuCut=Cu&t
menuCopy=&Copy
menuPaste=&Paste
*/
private MnemonicString mnemonic;
+ /**
+ * An optional 2-cell-wide picture/icon for this item.
+ */
+ private int icon = -1;
+
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
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);
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) {
}
}
+ 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);
+ }
}
// ------------------------------------------------------------------------
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.
*/
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.
*
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.
*
+++ /dev/null
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <groupId>com.gitlab.klamonte</groupId>
- <artifactId>jexer</artifactId>
- <packaging>jar</packaging>
- <name>Jexer</name>
- <description>Java Text User Interface library that resembles Turbo Vision</description>
- <version>1.0.0-SNAPSHOT</version>
- <url>https://gitlab.com/klamonte/jexer</url>
-
- <licenses>
- <license>
- <name>MIT License</name>
- <url>http://www.opensource.org/licenses/mit-license.php</url>
- <distribution>repo</distribution>
- </license>
- </licenses>
-
- <properties>
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
- </properties>
-
- <scm>
- <connection>scm:git:https://gitlab.com/klamonte/jexer.git</connection>
- <developerConnection>scm:git:https://gitlab.com/klamonte/jexer.git</developerConnection>
- <url>https://gitlab.com/klamonte/jexer</url>
- <tag>HEAD</tag>
- </scm>
-
- <issueManagement>
- <system>gitlab</system>
- <url>https://gitlab.com/klamonte/jexer/issues</url>
- </issueManagement>
-
- <distributionManagement>
- <snapshotRepository>
- <id>ossrh</id>
- <url>https://oss.sonatype.org/content/repositories/snapshots</url>
- </snapshotRepository>
- <repository>
- <id>ossrh</id>
- <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
- </repository>
- </distributionManagement>
-
- <build>
- <sourceDirectory>${project.basedir}/src</sourceDirectory>
- <resources>
- <resource>
- <directory>${project.basedir}/resources</directory>
- <filtering>false</filtering>
- <includes>
- <include>**/*</include>
- </includes>
- </resource>
- <resource>
- <directory>src</directory>
- <excludes>
- <exclude>**/*.java</exclude>
- </excludes>
- </resource>
- </resources>
-
- <plugins>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <version>3.1</version>
- <configuration>
- <source>1.6</source>
- <target>1.6</target>
- </configuration>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-jar-plugin</artifactId>
- <version>3.0.2</version>
- <configuration>
- <archive>
- <manifest>
- <mainClass>
- jexer.demos.Demo1
- </mainClass>
-
- </manifest>
- <manifestEntries>
- <Implementation-Version>${project.version}</Implementation-Version>
- </manifestEntries>
- </archive>
- </configuration>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-source-plugin</artifactId>
- <version>2.2.1</version>
- <executions>
- <execution>
- <id>attach-sources</id>
- <phase>verify</phase>
- <goals>
- <goal>jar-no-fork</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-javadoc-plugin</artifactId>
- <version>2.9.1</version>
- <executions>
- <execution>
- <id>attach-javadocs</id>
- <goals>
- <goal>jar</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-release-plugin</artifactId>
- <version>2.5.3</version>
- <configuration>
- <localCheckout>true</localCheckout>
- <pushChanges>false</pushChanges>
- <mavenExecutorId>forked-path</mavenExecutorId>
- <!-- <arguments>-Dgpg.passphrase=${gpg.passphrase}</arguments> -->
- </configuration>
-
- <!--
- <dependencies>
- <dependency>
- <groupId>org.apache.maven.scm</groupId>
- <artifactId>maven-scm-provider-gitexe</artifactId>
- <version>1.9.5</version>
- </dependency>
- </dependencies>
- -->
- </plugin>
-
- <plugin>
- <groupId>org.sonatype.plugins</groupId>
- <artifactId>nexus-staging-maven-plugin</artifactId>
- <version>1.6.7</version>
- <extensions>true</extensions>
- <configuration>
- <serverId>ossrh</serverId>
- <nexusUrl>https://oss.sonatype.org/</nexusUrl>
- <autoReleaseAfterClose>true</autoReleaseAfterClose>
- </configuration>
- </plugin>
- </plugins>
- </build>
-
- <profiles>
- <profile>
- <id>release-sign-artifacts</id>
- <activation>
- <property>
- <name>performRelease</name>
- <value>true</value>
- </property>
- </activation>
-
- <build>
- <plugins>
-
- <!--
- <plugin>
- <artifactId>maven-deploy-plugin</artifactId>
- <version>2.8.2</version>
- <executions>
- <execution>
- <id>default-deploy</id>
- <phase>deploy</phase>
- <goals>
- <goal>deploy</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- -->
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-gpg-plugin</artifactId>
- <version>1.5</version>
- <executions>
- <execution>
- <id>sign-artifacts</id>
- <phase>verify</phase>
- <goals>
- <goal>sign</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
- </profile>
- </profiles>
-
- <developers>
- <developer>
- <id>klamonte</id>
- <name>Kevin Lamonte</name>
- <email>kevin.lamonte@gmail.com</email>
- </developer>
- </developers>
-</project>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+
+<help>
+ <name>Jexer Help File</name>
+ <author>Kevin Lamonte</author>
+ <version>1.0.0</version>
+ <date>Jan 1, 2020</date>
+ <topics>
+ <topic title="Help">
+ <text>
+ This [window](Windows) does not have a specific help topic.
+ See [here](Help On Help) for general information on using the
+ help system.
+ </text>
+ </topic>
+ <topic title="Help On Help">
+ <text>
+ The #{help} system...
+
+ </text>
+ </topic>
+
+ <topic title="Menus">
+ <text>
+ #{Menus} do ...
+ </text>
+ </topic>
+
+ <topic title="Windows">
+ <text>
+ #{Windows} do ...
+ </text>
+ </topic>
+
+ <topic title="Editing Text">
+ <text>
+ The #{text editing} [window](Windows)...
+ </text>
+ </topic>
+
+ <topic title="Editing Tables">
+ <text></text>
+ </topic>
+
+ <topic title="Terminal Window">
+ <text>
+ The terminal window ...
+ </text>
+ </topic>
+
+ <topic title="Copyright Infomation">
+ <text>
+ Copyright (C) 2019 Kevin Lamonte
+
+ Available to all under the MIT License.
+ </text>
+ </topic>
+
+ </topics>
+</help>
+++ /dev/null
-/*
- * 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);
- }
-
-}
*/
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 -----------------------------------------------------------
// ------------------------------------------------------------------------
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");
}
}
+ /**
+ * 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;
}
return dirty;
}
+ /**
+ * Unset the dirty flag.
+ */
+ public void setNotDirty() {
+ dirty = false;
+ }
+
/**
* Save contents to file.
*
"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");
}
// 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();
}
// int line = lineNumber;
while ((getChar() == -1)
|| (getRawLine().length() == 0)
- || Character.isSpace((char) getChar())
+ || Character.isWhitespace((char) getChar())
) {
if (left() == false) {
return;
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.
}
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;
}
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.
}
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.
}
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;
}
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;
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--;
}
}
+ /**
+ * 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.
*
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();
+ }
+
}
* Public constructor sets the theme to the default.
*/
public Highlighter() {
- colors = new TreeMap<String, CellAttributes>();
+ // 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<String, CellAttributes>();
+ colors.putAll(rhs.colors);
+ }
+
/**
* See if this is a character that should split a word.
*
* @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;
}
* Sets to defaults that resemble the Borland IDE colors.
*/
public void setJavaColors() {
+ colors = new TreeMap<String, CellAttributes>();
+
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) {
import java.util.List;
import jexer.bits.CellAttributes;
+import jexer.bits.GraphicsChars;
import jexer.bits.StringUtils;
/**
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();
}
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.
*
}
/**
- * 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);
if (getDisplayLength() == 0) {
return false;
}
- if (position == getDisplayLength() - 1) {
+ if (screenPosition == getDisplayLength() - 1) {
return false;
}
if (position < rawText.length()) {
* @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;
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);
/**
* 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();
}
* @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));
* @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));
* @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;
}
" 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) == ' '));
+ }
+ }
+
}
* @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());
}
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();
+ }
+ }
+ }
+
}
*/
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;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import javax.imageio.ImageIO;
import jexer.TKeypress;
import jexer.backend.GlyphMaker;
/**
* The type of emulator to be.
*/
- private DeviceType type = DeviceType.VT102;
+ private final DeviceType type;
/**
* The scrollback buffer characters + attributes.
/**
* 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
* 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.
* 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.
/**
* Non-csi collect buffer.
*/
- private StringBuilder collectBuffer;
+ private StringBuilder collectBuffer = new StringBuilder(128);
/**
* When true, use the G1 character set.
/**
* Sixel collection buffer.
*/
- private StringBuilder sixelParseBuffer;
+ private StringBuilder sixelParseBuffer = new StringBuilder(2048);
/**
* Sixel shared palette.
*/
private ArrayList<TInputEvent> userQueue = new ArrayList<TInputEvent>();
+ /**
+ * 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.
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;
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);
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]);
+ }
}
}
}
// 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.
*
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);
}
// the input streams.
if (stopReaderThread == false) {
stopReaderThread = true;
- try {
- readerThread.join(1000);
- } catch (InterruptedException e) {
- // SQUASH
- }
}
// Now close the output stream.
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;
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.
*
*/
private void toGround() {
csiParams.clear();
- collectBuffer = new StringBuilder(8);
+ collectBuffer.setLength(0);
scanState = ScanState.GROUND;
}
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);
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);
+
}
/**
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;
arrowKeyMode = ArrowKeyMode.ANSI;
keypadMode = KeypadMode.Numeric;
wrapLineFlag = false;
- if (displayListener != null) {
- width = displayListener.getDisplayWidth();
- height = displayListener.getDisplayHeight();
- rightMargin = width - 1;
- }
// Flags
shiftOut = false;
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();
}
* Handle a linefeed.
*/
private void linefeed() {
-
if (currentState.cursorY < scrollRegionBottom) {
// Increment screen y
currentState.cursorY++;
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) {
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));
}
if (decPrivateModeFlag == true) {
if (value == true) {
// Enable sixel scrolling (default).
- // TODO
+ // Not supported
} else {
// Disable sixel scrolling.
- // TODO
+ // Not supported
}
}
}
* RGB color mode.
*/
rgbColor = true;
- break;
+ continue;
case 5:
/*
* Indexed color mode.
*/
idx88Color = true;
- break;
+ continue;
default:
/*
case 8:
// Invisible
- // TODO
+ // Not supported
break;
case 90:
// 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;
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);
}
}
- 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
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);
*
* @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
// 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
// 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
// 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
* @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;
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;
}
}
}
+ 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:
*
}
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;
// 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);
+ }
+
}
}
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
-import java.util.ArrayList;
import java.util.HashMap;
/**
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;
// 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;