Merge branch 'upstream-sep2019-tcombo' into subtree
authorNiki Roo <niki@nikiroo.be>
Thu, 2 Jan 2020 17:20:14 +0000 (18:20 +0100)
committerNiki Roo <niki@nikiroo.be>
Thu, 2 Jan 2020 17:20:14 +0000 (18:20 +0100)
198 files changed:
EditMenuUser.java [new file with mode: 0644]
LICENSE [deleted file]
README.md [deleted file]
Scrollable.java [moved from src/jexer/Scrollable.java with 100% similarity]
TAction.java [moved from src/jexer/TAction.java with 100% similarity]
TApplication.java [moved from src/jexer/TApplication.java with 90% similarity]
TApplication.properties [moved from src/jexer/TApplication.properties with 87% similarity]
TButton.java [moved from src/jexer/TButton.java with 97% similarity]
TCalendar.java [moved from src/jexer/TCalendar.java with 100% similarity]
TCheckBox.java [moved from src/jexer/TCheckBox.java with 100% similarity]
TComboBox.java [moved from src/jexer/TComboBox.java with 100% similarity]
TCommand.java [moved from src/jexer/TCommand.java with 100% similarity]
TDesktop.java [moved from src/jexer/TDesktop.java with 100% similarity]
TDirectoryList.java [moved from src/jexer/TDirectoryList.java with 100% similarity]
TEditColorThemeWindow.java [moved from src/jexer/TEditColorThemeWindow.java with 100% similarity]
TEditColorThemeWindow.properties [moved from src/jexer/TEditColorThemeWindow.properties with 100% similarity]
TEditorWidget.java [new file with mode: 0644]
TEditorWindow.java [moved from src/jexer/TEditorWindow.java with 92% similarity]
TEditorWindow.properties [moved from src/jexer/TEditorWindow.properties with 100% similarity]
TExceptionDialog.java [moved from src/jexer/TExceptionDialog.java with 97% similarity]
TExceptionDialog.properties [moved from src/jexer/TExceptionDialog.properties with 52% similarity]
TField.java [moved from src/jexer/TField.java with 90% similarity]
TFileOpenBox.java [moved from src/jexer/TFileOpenBox.java with 100% similarity]
TFileOpenBox.properties [moved from src/jexer/TFileOpenBox.properties with 100% similarity]
TFontChooserWindow.java [moved from src/jexer/TFontChooserWindow.java with 100% similarity]
TFontChooserWindow.properties [moved from src/jexer/TFontChooserWindow.properties with 100% similarity]
THScroller.java [moved from src/jexer/THScroller.java with 100% similarity]
THelpWindow.java [new file with mode: 0644]
THelpWindow.properties [new file with mode: 0644]
TImage.java [moved from src/jexer/TImage.java with 90% similarity]
TImageWindow.java [moved from src/jexer/TImageWindow.java with 100% similarity]
TImageWindow.properties [moved from src/jexer/TImageWindow.properties with 100% similarity]
TInputBox.java [moved from src/jexer/TInputBox.java with 100% similarity]
TKeypress.java [moved from src/jexer/TKeypress.java with 95% similarity]
TLabel.java [moved from src/jexer/TLabel.java with 100% similarity]
TList.java [moved from src/jexer/TList.java with 97% similarity]
TMessageBox.java [moved from src/jexer/TMessageBox.java with 100% similarity]
TMessageBox.properties [moved from src/jexer/TMessageBox.properties with 100% similarity]
TPanel.java [moved from src/jexer/TPanel.java with 100% similarity]
TPasswordField.java [moved from src/jexer/TPasswordField.java with 99% similarity]
TProgressBar.java [moved from src/jexer/TProgressBar.java with 100% similarity]
TRadioButton.java [moved from src/jexer/TRadioButton.java with 91% similarity]
TRadioGroup.java [moved from src/jexer/TRadioGroup.java with 69% similarity]
TScrollableWidget.java [moved from src/jexer/TScrollableWidget.java with 100% similarity]
TScrollableWindow.java [moved from src/jexer/TScrollableWindow.java with 100% similarity]
TSpinner.java [moved from src/jexer/TSpinner.java with 100% similarity]
TSplitPane.java [moved from src/jexer/TSplitPane.java with 90% similarity]
TStatusBar.java [moved from src/jexer/TStatusBar.java with 100% similarity]
TTableWidget.java [moved from src/jexer/TTableWidget.java with 99% similarity]
TTableWindow.java [moved from src/jexer/TTableWindow.java with 99% similarity]
TTableWindow.properties [moved from src/jexer/TTableWindow.properties with 100% similarity]
TTerminalWidget.java [moved from src/jexer/TTerminalWidget.java with 85% similarity]
TTerminalWidget.properties [moved from src/jexer/TTerminalWidget.properties with 100% similarity]
TTerminalWindow.java [moved from src/jexer/TTerminalWindow.java with 89% similarity]
TTerminalWindow.properties [moved from src/jexer/TTerminalWindow.properties with 64% similarity]
TText.java [moved from src/jexer/TText.java with 97% similarity]
TTimer.java [moved from src/jexer/TTimer.java with 100% similarity]
TVScroller.java [moved from src/jexer/TVScroller.java with 100% similarity]
TWidget.java [moved from src/jexer/TWidget.java with 98% similarity]
TWindow.java [moved from src/jexer/TWindow.java with 98% similarity]
backend/Backend.java [moved from src/jexer/backend/Backend.java with 100% similarity]
backend/ECMA48Backend.java [moved from src/jexer/backend/ECMA48Backend.java with 100% similarity]
backend/ECMA48Terminal.java [moved from src/jexer/backend/ECMA48Terminal.java with 92% similarity]
backend/GenericBackend.java [moved from src/jexer/backend/GenericBackend.java with 100% similarity]
backend/GlyphMaker.java [moved from src/jexer/backend/GlyphMaker.java with 98% similarity]
backend/HeadlessBackend.java [new file with mode: 0644]
backend/LogicalScreen.java [moved from src/jexer/backend/LogicalScreen.java with 83% similarity]
backend/MultiBackend.java [moved from src/jexer/backend/MultiBackend.java with 100% similarity]
backend/MultiScreen.java [moved from src/jexer/backend/MultiScreen.java with 84% similarity]
backend/Screen.java [moved from src/jexer/backend/Screen.java with 87% similarity]
backend/SessionInfo.java [moved from src/jexer/backend/SessionInfo.java with 100% similarity]
backend/SwingBackend.java [moved from src/jexer/backend/SwingBackend.java with 100% similarity]
backend/SwingComponent.java [moved from src/jexer/backend/SwingComponent.java with 96% similarity]
backend/SwingSessionInfo.java [moved from src/jexer/backend/SwingSessionInfo.java with 100% similarity]
backend/SwingTerminal.java [moved from src/jexer/backend/SwingTerminal.java with 94% similarity]
backend/TSessionInfo.java [moved from src/jexer/backend/TSessionInfo.java with 100% similarity]
backend/TTYSessionInfo.java [moved from src/jexer/backend/TTYSessionInfo.java with 100% similarity]
backend/TWindowBackend.java [moved from src/jexer/backend/TWindowBackend.java with 100% similarity]
backend/TerminalReader.java [moved from src/jexer/backend/TerminalReader.java with 100% similarity]
backend/package-info.java [moved from src/jexer/backend/package-info.java with 100% similarity]
bits/Cell.java [moved from src/jexer/bits/Cell.java with 99% similarity]
bits/CellAttributes.java [moved from src/jexer/bits/CellAttributes.java with 99% similarity]
bits/Clipboard.java [new file with mode: 0644]
bits/Color.java [moved from src/jexer/bits/Color.java with 100% similarity]
bits/ColorTheme.java [moved from src/jexer/bits/ColorTheme.java with 92% similarity]
bits/GraphicsChars.java [moved from src/jexer/bits/GraphicsChars.java with 100% similarity]
bits/MnemonicString.java [moved from src/jexer/bits/MnemonicString.java with 100% similarity]
bits/StringUtils.java [moved from src/jexer/bits/StringUtils.java with 61% similarity]
bits/package-info.java [moved from src/jexer/bits/package-info.java with 100% similarity]
build.xml [deleted file]
demos/Demo1.java [moved from src/jexer/demos/Demo1.java with 100% similarity]
demos/Demo2.java [moved from src/jexer/demos/Demo2.java with 100% similarity]
demos/Demo2.properties [moved from src/jexer/demos/Demo2.properties with 100% similarity]
demos/Demo3.java [moved from src/jexer/demos/Demo3.java with 100% similarity]
demos/Demo4.java [moved from src/jexer/demos/Demo4.java with 100% similarity]
demos/Demo5.java [moved from src/jexer/demos/Demo5.java with 100% similarity]
demos/Demo5.properties [moved from src/jexer/demos/Demo5.properties with 100% similarity]
demos/Demo6.java [moved from src/jexer/demos/Demo6.java with 99% similarity]
demos/Demo6.properties [moved from src/jexer/demos/Demo6.properties with 100% similarity]
demos/Demo7.java [moved from src/jexer/demos/Demo7.java with 100% similarity]
demos/Demo7.properties [moved from src/jexer/demos/Demo7.properties with 100% similarity]
demos/Demo8.java [new file with mode: 0644]
demos/Demo8.properties [new file with mode: 0644]
demos/DemoApplication.java [moved from src/jexer/demos/DemoApplication.java with 100% similarity]
demos/DemoApplication.properties [moved from src/jexer/demos/DemoApplication.properties with 100% similarity]
demos/DemoCheckBoxWindow.java [moved from src/jexer/demos/DemoCheckBoxWindow.java with 98% similarity]
demos/DemoCheckBoxWindow.properties [moved from src/jexer/demos/DemoCheckBoxWindow.properties with 100% similarity]
demos/DemoEditorWindow.java [moved from src/jexer/demos/DemoEditorWindow.java with 100% similarity]
demos/DemoEditorWindow.properties [moved from src/jexer/demos/DemoEditorWindow.properties with 100% similarity]
demos/DemoMainWindow.java [moved from src/jexer/demos/DemoMainWindow.java with 100% similarity]
demos/DemoMainWindow.properties [moved from src/jexer/demos/DemoMainWindow.properties with 100% similarity]
demos/DemoMsgBoxWindow.java [moved from src/jexer/demos/DemoMsgBoxWindow.java with 100% similarity]
demos/DemoMsgBoxWindow.properties [moved from src/jexer/demos/DemoMsgBoxWindow.properties with 100% similarity]
demos/DemoTableWindow.java [moved from src/jexer/demos/DemoTableWindow.java with 100% similarity]
demos/DemoTableWindow.properties [moved from src/jexer/demos/DemoTableWindow.properties with 100% similarity]
demos/DemoTextFieldWindow.java [moved from src/jexer/demos/DemoTextFieldWindow.java with 100% similarity]
demos/DemoTextFieldWindow.properties [moved from src/jexer/demos/DemoTextFieldWindow.properties with 100% similarity]
demos/DemoTextWindow.java [moved from src/jexer/demos/DemoTextWindow.java with 100% similarity]
demos/DemoTextWindow.properties [moved from src/jexer/demos/DemoTextWindow.properties with 100% similarity]
demos/DemoTreeViewWindow.java [moved from src/jexer/demos/DemoTreeViewWindow.java with 100% similarity]
demos/DemoTreeViewWindow.properties [moved from src/jexer/demos/DemoTreeViewWindow.properties with 100% similarity]
demos/DesktopDemo.java [moved from src/jexer/demos/DesktopDemo.java with 100% similarity]
demos/DesktopDemoApplication.java [moved from src/jexer/demos/DesktopDemoApplication.java with 100% similarity]
demos/DesktopDemoApplication.properties [moved from src/jexer/demos/DesktopDemoApplication.properties with 100% similarity]
demos/package-info.java [moved from src/jexer/demos/package-info.java with 100% similarity]
docs/032_announcement.txt [deleted file]
docs/images.md [deleted file]
docs/images2.md [deleted file]
event/TCommandEvent.java [moved from src/jexer/event/TCommandEvent.java with 100% similarity]
event/TInputEvent.java [moved from src/jexer/event/TInputEvent.java with 100% similarity]
event/TKeypressEvent.java [moved from src/jexer/event/TKeypressEvent.java with 100% similarity]
event/TMenuEvent.java [moved from src/jexer/event/TMenuEvent.java with 100% similarity]
event/TMouseEvent.java [moved from src/jexer/event/TMouseEvent.java with 87% similarity]
event/TResizeEvent.java [moved from src/jexer/event/TResizeEvent.java with 100% similarity]
event/package-info.java [moved from src/jexer/event/package-info.java with 100% similarity]
examples/HelloWorld.java [deleted file]
examples/JexerImageViewer.java [deleted file]
examples/JexerTilingWindowManager.java [deleted file]
examples/JexerTilingWindowManager2.java [deleted file]
examples/MyApplication.java [deleted file]
examples/imgls [new file with mode: 0755]
help/HelpFile.java [new file with mode: 0644]
help/HelpFile.properties [new file with mode: 0644]
help/Link.java [new file with mode: 0644]
help/THelpText.java [new file with mode: 0644]
help/TParagraph.java [new file with mode: 0644]
help/TWord.java [new file with mode: 0644]
help/Topic.java [new file with mode: 0644]
help/Topic.properties [new file with mode: 0644]
help/package-info.java [new file with mode: 0644]
io/ReadTimeoutException.java [moved from src/jexer/io/ReadTimeoutException.java with 100% similarity]
io/TimeoutInputStream.java [moved from src/jexer/io/TimeoutInputStream.java with 99% similarity]
io/package-info.java [moved from src/jexer/io/package-info.java with 100% similarity]
layout/BoxLayoutManager.java [moved from src/jexer/layout/BoxLayoutManager.java with 100% similarity]
layout/LayoutManager.java [moved from src/jexer/layout/LayoutManager.java with 100% similarity]
layout/StretchLayoutManager.java [moved from src/jexer/layout/StretchLayoutManager.java with 97% similarity]
layout/package-info.java [moved from src/jexer/layout/package-info.java with 100% similarity]
menu/TMenu.java [moved from src/jexer/menu/TMenu.java with 94% similarity]
menu/TMenu.properties [moved from src/jexer/menu/TMenu.properties with 98% similarity]
menu/TMenuItem.java [moved from src/jexer/menu/TMenuItem.java with 85% similarity]
menu/TMenuSeparator.java [moved from src/jexer/menu/TMenuSeparator.java with 100% similarity]
menu/TSubMenu.java [moved from src/jexer/menu/TSubMenu.java with 90% similarity]
menu/package-info.java [moved from src/jexer/menu/package-info.java with 100% similarity]
net/TelnetInputStream.java [moved from src/jexer/net/TelnetInputStream.java with 100% similarity]
net/TelnetOutputStream.java [moved from src/jexer/net/TelnetOutputStream.java with 100% similarity]
net/TelnetServerSocket.java [moved from src/jexer/net/TelnetServerSocket.java with 100% similarity]
net/TelnetSocket.java [moved from src/jexer/net/TelnetSocket.java with 100% similarity]
net/package-info.java [moved from src/jexer/net/package-info.java with 100% similarity]
package-info.java [moved from src/jexer/package-info.java with 100% similarity]
pom.xml [deleted file]
resources/help.xml [new file with mode: 0644]
screenshots/jexer_sixel_in_sixel.png [deleted file]
screenshots/new_demo1.png [deleted file]
screenshots/readme_application.png [deleted file]
screenshots/screenshot1_old.png [moved from screenshots/screenshot1.png with 100% similarity]
screenshots/sixel_color_wheel.png [deleted file]
screenshots/sixel_images.png [deleted file]
screenshots/snake_swing.png [deleted file]
screenshots/snake_xterm.png [deleted file]
screenshots/yodawg.png [deleted file]
src/jexer/TEditorWidget.java [deleted file]
teditor/Document.java [moved from src/jexer/teditor/Document.java with 77% similarity]
teditor/Highlighter.java [moved from src/jexer/teditor/Highlighter.java with 76% similarity]
teditor/Line.java [moved from src/jexer/teditor/Line.java with 70% similarity]
teditor/Word.java [moved from src/jexer/teditor/Word.java with 96% similarity]
teditor/package-info.java [moved from src/jexer/teditor/package-info.java with 100% similarity]
tterminal/DECCharacterSets.java [moved from src/jexer/tterminal/DECCharacterSets.java with 100% similarity]
tterminal/DisplayLine.java [moved from src/jexer/tterminal/DisplayLine.java with 92% similarity]
tterminal/DisplayListener.java [moved from src/jexer/tterminal/DisplayListener.java with 100% similarity]
tterminal/ECMA48.java [moved from src/jexer/tterminal/ECMA48.java with 91% similarity]
tterminal/Sixel.java [moved from src/jexer/tterminal/Sixel.java with 99% similarity]
tterminal/package-info.java [moved from src/jexer/tterminal/package-info.java with 100% similarity]
ttree/TDirectoryTreeItem.java [moved from src/jexer/ttree/TDirectoryTreeItem.java with 100% similarity]
ttree/TTreeItem.java [moved from src/jexer/ttree/TTreeItem.java with 100% similarity]
ttree/TTreeView.java [moved from src/jexer/ttree/TTreeView.java with 100% similarity]
ttree/TTreeViewWidget.java [moved from src/jexer/ttree/TTreeViewWidget.java with 92% similarity]
ttree/TTreeViewWindow.java [moved from src/jexer/ttree/TTreeViewWindow.java with 100% similarity]
ttree/package-info.java [moved from src/jexer/ttree/package-info.java with 100% similarity]

diff --git a/EditMenuUser.java b/EditMenuUser.java
new file mode 100644 (file)
index 0000000..52dc33e
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+/**
+ * EditMenuUser is used by TApplication to enable/disable edit menu items.  A
+ * widget that supports these functions should define an onCommand method
+ * that operates on cmCut, cmCopy, cmPaste, and cmClear.
+ */
+public interface EditMenuUser {
+
+    /**
+     * Check if the cut menu item should be enabled.
+     *
+     * @return true if the cut menu item should be enabled
+     */
+    public boolean isEditMenuCut();
+
+    /**
+     * Check if the copy menu item should be enabled.
+     *
+     * @return true if the copy menu item should be enabled
+     */
+    public boolean isEditMenuCopy();
+
+    /**
+     * Check if the paste menu item should be enabled.
+     *
+     * @return true if the paste menu item should be enabled
+     */
+    public boolean isEditMenuPaste();
+
+    /**
+     * Check if the clear menu item should be enabled.
+     *
+     * @return true if the clear menu item should be enabled
+     */
+    public boolean isEditMenuClear();
+
+}
diff --git a/LICENSE b/LICENSE
deleted file mode 100644 (file)
index 7b02f56..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,22 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2013-2019 Kevin Lamonte
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
deleted file mode 100644 (file)
index 4e6127a..0000000
--- a/README.md
+++ /dev/null
@@ -1,201 +0,0 @@
-Jexer - Java Text User Interface library
-========================================
-
-This library implements a text-based windowing system loosely
-reminiscent of Borland's [Turbo
-Vision](http://en.wikipedia.org/wiki/Turbo_Vision) system.  It looks
-like this:
-
-![Terminal, Image, Table](/screenshots/new_demo1.png?raw=true "Terminal, Image, Table")
-
-Jexer works on both Xterm-like terminals and Swing, and supports
-images in both Xterm and Swing.  On Swing, images are true color:
-
-![Swing Snake Image](/screenshots/snake_swing.png?raw=true "Swing Snake Image")
-
-On Xterm, images are dithered to a common palette:
-
-![Xterm Snake Image](/screenshots/snake_xterm.png?raw=true "Xterm Snake Image")
-
-
-
-License
--------
-
-Jexer is available to all under the MIT License.  See the file LICENSE
-for the full license text.
-
-
-
-Obtaining Jexer
----------------
-
-Jexer is available on Maven Central:
-
-```xml
-<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/) .
similarity index 100%
rename from src/jexer/Scrollable.java
rename to Scrollable.java
similarity index 100%
rename from src/jexer/TAction.java
rename to TAction.java
similarity index 90%
rename from src/jexer/TApplication.java
rename to TApplication.java
index 9d27c10f5420052103cee046baae697ca8c2bd6e..28e35091ded6e1ef006190574e945c0426c41057 100644 (file)
@@ -29,6 +29,7 @@
 package jexer;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.InputStream;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -47,6 +48,7 @@ import java.util.ResourceBundle;
 
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
 import jexer.bits.ColorTheme;
 import jexer.bits.StringUtils;
 import jexer.event.TCommandEvent;
@@ -61,6 +63,8 @@ import jexer.backend.Screen;
 import jexer.backend.SwingBackend;
 import jexer.backend.ECMA48Backend;
 import jexer.backend.TWindowBackend;
+import jexer.help.HelpFile;
+import jexer.help.Topic;
 import jexer.menu.TMenu;
 import jexer.menu.TMenuItem;
 import jexer.menu.TSubMenu;
@@ -148,6 +152,11 @@ public class TApplication implements Runnable {
      */
     private Backend backend;
 
+    /**
+     * The clipboard for copy and paste.
+     */
+    private Clipboard clipboard = new Clipboard();
+
     /**
      * Actual mouse coordinate X.
      */
@@ -158,16 +167,6 @@ public class TApplication implements Runnable {
      */
     private int mouseY;
 
-    /**
-     * Old version of mouse coordinate X.
-     */
-    private int oldMouseX;
-
-    /**
-     * Old version mouse coordinate Y.
-     */
-    private int oldMouseY;
-
     /**
      * Old drawn version of mouse coordinate X.
      */
@@ -240,11 +239,6 @@ public class TApplication implements Runnable {
      */
     private List<TWindow> windows;
 
-    /**
-     * The currently acive window.
-     */
-    private TWindow activeWindow = null;
-
     /**
      * Timers that are being ticked.
      */
@@ -325,6 +319,46 @@ public class TApplication implements Runnable {
      */
     private long screenResizeTime = 0;
 
+    /**
+     * If true, screen selection is a rectangle.
+     */
+    private boolean screenSelectionRectangle = false;
+
+    /**
+     * If true, the mouse is dragging a screen selection.
+     */
+    private boolean inScreenSelection = false;
+
+    /**
+     * Screen selection starting X.
+     */
+    private int screenSelectionX0;
+
+    /**
+     * Screen selection starting Y.
+     */
+    private int screenSelectionY0;
+
+    /**
+     * Screen selection ending X.
+     */
+    private int screenSelectionX1;
+
+    /**
+     * Screen selection ending Y.
+     */
+    private int screenSelectionY1;
+
+    /**
+     * The help file data.  Note package private access.
+     */
+    HelpFile helpFile;
+
+    /**
+     * The stack of help topics.  Note package private access.
+     */
+    ArrayList<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
@@ -773,6 +807,27 @@ public class TApplication implements Runnable {
             }
         }
 
+        // Load the help system
+        invokeLater(new Runnable() {
+            /*
+             * This isn't the best solution.  But basically if a TApplication
+             * subclass constructor throws and needs to use TExceptionDialog,
+             * it may end up at the bottom of the window stack with a bunch
+             * of modal windows on top of it if said constructors spawn their
+             * windows also via invokeLater().  But if they don't do that,
+             * and instead just conventionally construct their windows, then
+             * this exception dialog will end up on top where it should be.
+             */
+            public void run() {
+                try {
+                    ClassLoader loader = Thread.currentThread().getContextClassLoader();
+                    helpFile = new HelpFile();
+                    helpFile.load(loader.getResourceAsStream("help.xml"));
+                } catch (Exception e) {
+                    new TExceptionDialog(TApplication.this, e);
+                }
+            }
+        });
     }
 
     // ------------------------------------------------------------------------
@@ -904,6 +959,15 @@ public class TApplication implements Runnable {
             return true;
         }
 
+        if (command.equals(cmHelp)) {
+            if (getActiveWindow() != null) {
+                new THelpWindow(this, getActiveWindow().getHelpTopic());
+            } else {
+                new THelpWindow(this);
+            }
+            return true;
+        }
+
         if (command.equals(cmShell)) {
             openTerminal(0, 0, TWindow.RESIZABLE);
             return true;
@@ -955,6 +1019,62 @@ public class TApplication implements Runnable {
             return true;
         }
 
+        if (menu.getId() == TMenu.MID_HELP_HELP) {
+            new THelpWindow(this, THelpWindow.HELP_HELP);
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_CONTENTS) {
+            new THelpWindow(this, helpFile.getTableOfContents());
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_INDEX) {
+            new THelpWindow(this, helpFile.getIndex());
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_SEARCH) {
+            TInputBox inputBox = inputBox(i18n.
+                getString("searchHelpInputBoxTitle"),
+                i18n.getString("searchHelpInputBoxCaption"), "",
+                TInputBox.Type.OKCANCEL);
+            if (inputBox.isOk()) {
+                new THelpWindow(this,
+                    helpFile.getSearchResults(inputBox.getText()));
+            }
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_PREVIOUS) {
+            if (helpTopics.size() > 1) {
+                Topic previous = helpTopics.remove(helpTopics.size() - 2);
+                helpTopics.remove(helpTopics.size() - 1);
+                new THelpWindow(this, previous);
+            } else {
+                new THelpWindow(this, helpFile.getTableOfContents());
+            }
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_ACTIVE_FILE) {
+            try {
+                List<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;
@@ -989,6 +1109,24 @@ public class TApplication implements Runnable {
             new TFontChooserWindow(this);
             return true;
         }
+
+        if (menu.getId() == TMenu.MID_CUT) {
+            postMenuEvent(new TCommandEvent(cmCut));
+            return true;
+        }
+        if (menu.getId() == TMenu.MID_COPY) {
+            postMenuEvent(new TCommandEvent(cmCopy));
+            return true;
+        }
+        if (menu.getId() == TMenu.MID_PASTE) {
+            postMenuEvent(new TCommandEvent(cmPaste));
+            return true;
+        }
+        if (menu.getId() == TMenu.MID_CLEAR) {
+            postMenuEvent(new TCommandEvent(cmClear));
+            return true;
+        }
+
         return false;
     }
 
@@ -1035,6 +1173,48 @@ public class TApplication implements Runnable {
                 Thread.currentThread() + " finishEventProcessing()\n");
         }
 
+        // See if we need to enable/disable the edit menu.
+        EditMenuUser widget = null;
+        if (activeMenu == null) {
+            TWindow activeWindow = getActiveWindow();
+            if (activeWindow != null) {
+                if (activeWindow.getActiveChild() instanceof EditMenuUser) {
+                    widget = (EditMenuUser) activeWindow.getActiveChild();
+                }
+            } else if (desktop != null) {
+                if (desktop.getActiveChild() instanceof EditMenuUser) {
+                    widget = (EditMenuUser) desktop.getActiveChild();
+                }
+            }
+            if (widget == null) {
+                disableMenuItem(TMenu.MID_CUT);
+                disableMenuItem(TMenu.MID_COPY);
+                disableMenuItem(TMenu.MID_PASTE);
+                disableMenuItem(TMenu.MID_CLEAR);
+            } else {
+                if (widget.isEditMenuCut()) {
+                    enableMenuItem(TMenu.MID_CUT);
+                } else {
+                    disableMenuItem(TMenu.MID_CUT);
+                }
+                if (widget.isEditMenuCopy()) {
+                    enableMenuItem(TMenu.MID_COPY);
+                } else {
+                    disableMenuItem(TMenu.MID_COPY);
+                }
+                if (widget.isEditMenuPaste()) {
+                    enableMenuItem(TMenu.MID_PASTE);
+                } else {
+                    disableMenuItem(TMenu.MID_PASTE);
+                }
+                if (widget.isEditMenuClear()) {
+                    enableMenuItem(TMenu.MID_CLEAR);
+                } else {
+                    disableMenuItem(TMenu.MID_CLEAR);
+                }
+            }
+        }
+
         // Process timers and call doIdle()'s
         doIdle();
 
@@ -1101,8 +1281,6 @@ public class TApplication implements Runnable {
                     }
                     mouseX = 0;
                     mouseY = 0;
-                    oldMouseX = 0;
-                    oldMouseY = 0;
                 }
                 if (desktop != null) {
                     desktop.setDimensions(0, desktopTop, resize.getWidth(),
@@ -1157,9 +1335,29 @@ public class TApplication implements Runnable {
             typingHidMouse = false;
 
             TMouseEvent mouse = (TMouseEvent) event;
+            if (mouse.isMouse1() && (mouse.isShift() || mouse.isCtrl())) {
+                // Screen selection.
+                if (inScreenSelection) {
+                    screenSelectionX1 = mouse.getX();
+                    screenSelectionY1 = mouse.getY();
+                } else {
+                    inScreenSelection = true;
+                    screenSelectionX0 = mouse.getX();
+                    screenSelectionY0 = mouse.getY();
+                    screenSelectionX1 = mouse.getX();
+                    screenSelectionY1 = mouse.getY();
+                    screenSelectionRectangle = mouse.isCtrl();
+                }
+            } else {
+                if (inScreenSelection) {
+                    getScreen().copySelection(clipboard, screenSelectionX0,
+                        screenSelectionY0, screenSelectionX1, screenSelectionY1,
+                        screenSelectionRectangle);
+                }
+                inScreenSelection = false;
+            }
+
             if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
-                oldMouseX = mouseX;
-                oldMouseY = mouseY;
                 mouseX = mouse.getX();
                 mouseY = mouse.getY();
             } else {
@@ -1177,7 +1375,8 @@ public class TApplication implements Runnable {
                             mouse.getAbsoluteX(), mouse.getAbsoluteY(),
                             mouse.isMouse1(), mouse.isMouse2(),
                             mouse.isMouse3(),
-                            mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+                            mouse.isMouseWheelUp(), mouse.isMouseWheelDown(),
+                            mouse.isAlt(), mouse.isCtrl(), mouse.isShift());
 
                     } else {
                         // The first click of a potential double-click.
@@ -1235,6 +1434,7 @@ public class TApplication implements Runnable {
             // shortcutted by the active window, and if so dispatch the menu
             // event.
             boolean windowWillShortcut = false;
+            TWindow activeWindow = getActiveWindow();
             if (activeWindow != null) {
                 assert (activeWindow.isShown());
                 if (activeWindow.isShortcutKeypress(keypress.getKey())) {
@@ -1279,7 +1479,7 @@ public class TApplication implements Runnable {
 
         // Dispatch events to the active window -------------------------------
         boolean dispatchToDesktop = true;
-        TWindow window = activeWindow;
+        TWindow window = getActiveWindow();
         if (window != null) {
             assert (window.isActive());
             assert (window.isShown());
@@ -1347,8 +1547,6 @@ public class TApplication implements Runnable {
 
             TMouseEvent mouse = (TMouseEvent) event;
             if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
-                oldMouseX = mouseX;
-                oldMouseY = mouseY;
                 mouseX = mouse.getX();
                 mouseY = mouse.getY();
             } else {
@@ -1366,7 +1564,8 @@ public class TApplication implements Runnable {
                             mouse.getAbsoluteX(), mouse.getAbsoluteY(),
                             mouse.isMouse1(), mouse.isMouse2(),
                             mouse.isMouse3(),
-                            mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+                            mouse.isMouseWheelUp(), mouse.isMouseWheelDown(),
+                            mouse.isAlt(), mouse.isCtrl(), mouse.isShift());
 
                     } else {
                         // The first click of a potential double-click.
@@ -1474,13 +1673,17 @@ public class TApplication implements Runnable {
             desktop.onIdle();
         }
 
-        // Run any invokeLaters
+        // Run any invokeLaters.  We make a copy, and run that, because one
+        // of these Runnables might add call TApplication.invokeLater().
+        List<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();
 
     }
 
@@ -1582,6 +1785,15 @@ public class TApplication implements Runnable {
         return theme;
     }
 
+    /**
+     * Get the clipboard.
+     *
+     * @return the clipboard
+     */
+    public final Clipboard getClipboard() {
+        return clipboard;
+    }
+
     /**
      * Repaint the screen on the next update.
      */
@@ -1638,7 +1850,12 @@ public class TApplication implements Runnable {
      * @return the active window, or null if it is not set
      */
     public final TWindow getActiveWindow() {
-        return activeWindow;
+        for (TWindow window: windows) {
+            if (window.isShown() && window.isActive()) {
+                return window;
+            }
+        }
+        return null;
     }
 
     /**
@@ -1679,7 +1896,7 @@ public class TApplication implements Runnable {
         String version = getClass().getPackage().getImplementationVersion();
         if (version == null) {
             // This is Java 9+, use a hardcoded string here.
-            version = "0.3.2";
+            version = "1.0.0";
         }
         messageBox(i18n.getString("aboutDialogTitle"),
             MessageFormat.format(i18n.getString("aboutDialogText"), version),
@@ -1724,27 +1941,16 @@ public class TApplication implements Runnable {
     // ------------------------------------------------------------------------
 
     /**
-     * Invert the cell color at a position.  This is used to track the mouse.
-     *
-     * @param x column position
-     * @param y row position
-     */
-    private void invertCell(final int x, final int y) {
-        invertCell(x, y, false);
-    }
-
-    /**
-     * Invert the cell color at a position.  This is used to track the mouse.
+     * Draw the text mouse at position.
      *
      * @param x column position
      * @param y row position
-     * @param onlyThisCell if true, only invert this cell
      */
-    private void invertCell(final int x, final int y,
-        final boolean onlyThisCell) {
+    private void drawTextMouse(final int x, final int y) {
+        TWindow activeWindow = getActiveWindow();
 
         if (debugThreads) {
-            System.err.printf("%d %s invertCell() %d %d\n",
+            System.err.printf("%d %s drawTextMouse() %d %d\n",
                 System.currentTimeMillis(), Thread.currentThread(), x, y);
 
             if (activeWindow != null) {
@@ -1779,44 +1985,7 @@ public class TApplication implements Runnable {
             }
         }
 
-        Cell cell = getScreen().getCharXY(x, y);
-        if (cell.isImage()) {
-            cell.invertImage();
-        }
-        if (cell.getForeColorRGB() < 0) {
-            cell.setForeColor(cell.getForeColor().invert());
-        } else {
-            cell.setForeColorRGB(cell.getForeColorRGB() ^ 0x00ffffff);
-        }
-        if (cell.getBackColorRGB() < 0) {
-            cell.setBackColor(cell.getBackColor().invert());
-        } else {
-            cell.setBackColorRGB(cell.getBackColorRGB() ^ 0x00ffffff);
-        }
-        getScreen().putCharXY(x, y, cell);
-        if ((onlyThisCell == true) || (cell.getWidth() == Cell.Width.SINGLE)) {
-            return;
-        }
-
-        // This cell is one half of a fullwidth glyph.  Invert the other
-        // half.
-        if (cell.getWidth() == Cell.Width.LEFT) {
-            if (x < getScreen().getWidth() - 1) {
-                Cell rightHalf = getScreen().getCharXY(x + 1, y);
-                if (rightHalf.getWidth() == Cell.Width.RIGHT) {
-                    invertCell(x + 1, y, true);
-                    return;
-                }
-            }
-        }
-        if (cell.getWidth() == Cell.Width.RIGHT) {
-            if (x > 0) {
-                Cell leftHalf = getScreen().getCharXY(x - 1, y);
-                if (leftHalf.getWidth() == Cell.Width.LEFT) {
-                    invertCell(x - 1, y, true);
-                }
-            }
-        }
+        getScreen().invertCell(x, y);
     }
 
     /**
@@ -1876,9 +2045,15 @@ public class TApplication implements Runnable {
                     }
                 }
 
+                if (inScreenSelection) {
+                    getScreen().setSelection(screenSelectionX0,
+                        screenSelectionY0, screenSelectionX1, screenSelectionY1,
+                        screenSelectionRectangle);
+                }
+
                 if ((textMouse == true) && (typingHidMouse == false)) {
                     // Draw mouse at the new position.
-                    invertCell(mouseX, mouseY);
+                    drawTextMouse(mouseX, mouseY);
                 }
 
                 oldDrawnMouseX = mouseX;
@@ -1972,7 +2147,9 @@ public class TApplication implements Runnable {
             // Draw the status bar of the top-level window
             TStatusBar statusBar = null;
             if (topLevel != null) {
-                statusBar = topLevel.getStatusBar();
+                if (topLevel.isShown()) {
+                    statusBar = topLevel.getStatusBar();
+                }
             }
             if (statusBar != null) {
                 getScreen().resetClipping();
@@ -2013,8 +2190,14 @@ public class TApplication implements Runnable {
                 getScreen().unsetImageRow(mouseY);
             }
         }
+
+        if (inScreenSelection) {
+            getScreen().setSelection(screenSelectionX0, screenSelectionY0,
+                screenSelectionX1, screenSelectionY1, screenSelectionRectangle);
+        }
+
         if ((textMouse == true) && (typingHidMouse == false)) {
-            invertCell(mouseX, mouseY);
+            drawTextMouse(mouseX, mouseY);
         }
         oldDrawnMouseX = mouseX;
         oldDrawnMouseY = mouseY;
@@ -2154,7 +2337,7 @@ public class TApplication implements Runnable {
      *
      * @param window the window to become the new active window
      */
-    public void activateWindow(final TWindow window) {
+    public final void activateWindow(final TWindow window) {
         if (hasWindow(window) == false) {
             /*
              * Someone has a handle to a window I don't have.  Ignore this
@@ -2163,68 +2346,61 @@ public class TApplication implements Runnable {
             return;
         }
 
-        // Whatever window might be moving/dragging, stop it now.
-        for (TWindow w: windows) {
-            if (w.inMovements()) {
-                w.stopMovements();
-            }
+        if (modalWindowActive() && !window.isModal()) {
+            // Do not activate a non-modal on top of a modal.
+            return;
         }
 
-        assert (windows.size() > 0);
+        synchronized (windows) {
+            // Whatever window might be moving/dragging, stop it now.
+            for (TWindow w: windows) {
+                if (w.inMovements()) {
+                    w.stopMovements();
+                }
+            }
 
-        if (window.isHidden()) {
-            // Unhiding will also activate.
-            showWindow(window);
-            return;
-        }
-        assert (window.isShown());
+            assert (windows.size() > 0);
 
-        if (windows.size() == 1) {
-            assert (window == windows.get(0));
-            if (activeWindow == null) {
-                activeWindow = window;
-                window.setZ(0);
-                activeWindow.setActive(true);
-                activeWindow.onFocus();
+            if (window.isHidden()) {
+                // Unhiding will also activate.
+                showWindow(window);
+                return;
             }
+            assert (window.isShown());
 
-            assert (window.isActive());
-            assert (activeWindow == window);
-            return;
-        }
+            if (windows.size() == 1) {
+                assert (window == windows.get(0));
+                window.setZ(0);
+                window.setActive(true);
+                window.onFocus();
+                return;
+            }
 
-        if (activeWindow == window) {
-            assert (window.isActive());
+            if (getActiveWindow() == window) {
+                assert (window.isActive());
 
-            // Window is already active, do nothing.
-            return;
-        }
+                // Window is already active, do nothing.
+                return;
+            }
 
-        assert (!window.isActive());
-        if (activeWindow != null) {
-            activeWindow.setActive(false);
+            assert (!window.isActive());
 
-            // Increment every window Z that is on top of window
+            window.setZ(-1);
+            Collections.sort(windows);
+            int newZ = 0;
             for (TWindow w: windows) {
-                if (w == window) {
-                    continue;
-                }
-                if (w.getZ() < window.getZ()) {
-                    w.setZ(w.getZ() + 1);
+                w.setZ(newZ);
+                newZ++;
+                if ((w != window) && w.isActive()) {
+                    w.onUnfocus();
                 }
+                w.setActive(false);
             }
+            window.setActive(true);
+            window.onFocus();
+
+        } // synchronized (windows)
 
-            // Unset activeWindow now before unfocus, so that a window
-            // lifecycle change inside onUnfocus() doesn't call
-            // switchWindow() and lead to a stack overflow.
-            TWindow oldActiveWindow = activeWindow;
-            activeWindow = null;
-            oldActiveWindow.onUnfocus();
-        }
-        activeWindow = window;
-        activeWindow.setZ(0);
-        activeWindow.setActive(true);
-        activeWindow.onFocus();
         return;
     }
 
@@ -2242,28 +2418,39 @@ public class TApplication implements Runnable {
             return;
         }
 
-        // Whatever window might be moving/dragging, stop it now.
-        for (TWindow w: windows) {
-            if (w.inMovements()) {
-                w.stopMovements();
+        synchronized (windows) {
+
+            // Whatever window might be moving/dragging, stop it now.
+            for (TWindow w: windows) {
+                if (w.inMovements()) {
+                    w.stopMovements();
+                }
             }
-        }
 
-        assert (windows.size() > 0);
+            assert (windows.size() > 0);
 
-        if (!window.hidden) {
-            if (window == activeWindow) {
-                if (shownWindowCount() > 1) {
-                    switchWindow(true);
-                } else {
-                    activeWindow = null;
-                    window.setActive(false);
-                    window.onUnfocus();
-                }
+            if (window.hidden) {
+                return;
             }
+
+            window.setActive(false);
             window.hidden = true;
             window.onHide();
-        }
+
+            TWindow activeWindow = null;
+            for (TWindow w: windows) {
+                if (w.isShown()) {
+                    activeWindow = w;
+                    break;
+                }
+            }
+            assert (activeWindow != window);
+            if (activeWindow != null) {
+                activateWindow(activeWindow);
+            }
+
+        } // synchronized (windows)
+
     }
 
     /**
@@ -2280,25 +2467,16 @@ public class TApplication implements Runnable {
             return;
         }
 
-        // Whatever window might be moving/dragging, stop it now.
-        for (TWindow w: windows) {
-            if (w.inMovements()) {
-                w.stopMovements();
-            }
-        }
-
-        assert (windows.size() > 0);
-
         if (window.hidden) {
             window.hidden = false;
             window.onShow();
             activateWindow(window);
         }
+
     }
 
     /**
-     * Close window.  Note that the window's destructor is NOT called by this
-     * method, instead the GC is assumed to do the cleanup.
+     * Close window.
      *
      * @param window the window to remove
      */
@@ -2316,23 +2494,16 @@ public class TApplication implements Runnable {
         window.onPreClose();
 
         synchronized (windows) {
-            // Whatever window might be moving/dragging, stop it now.
-            for (TWindow w: windows) {
-                if (w.inMovements()) {
-                    w.stopMovements();
-                }
-            }
 
-            int z = window.getZ();
-            window.setZ(-1);
+            window.stopMovements();
             window.onUnfocus();
             windows.remove(window);
             Collections.sort(windows);
-            activeWindow = null;
-            int newZ = 0;
-            boolean foundNextWindow = false;
 
+            TWindow nextWindow = null;
+            int newZ = 0;
             for (TWindow w: windows) {
+                w.stopMovements();
                 w.setZ(newZ);
                 newZ++;
 
@@ -2340,22 +2511,22 @@ public class TApplication implements Runnable {
                 if (w.isHidden()) {
                     continue;
                 }
-
-                if (foundNextWindow == false) {
-                    foundNextWindow = true;
-                    w.setActive(true);
-                    w.onFocus();
-                    assert (activeWindow == null);
-                    activeWindow = w;
-                    continue;
+                if (nextWindow == null) {
+                    nextWindow = w;
+                } else {
+                    if (w.isActive()) {
+                        w.setActive(false);
+                        w.onUnfocus();
+                    }
                 }
+            }
 
-                if (w.isActive()) {
-                    w.setActive(false);
-                    w.onUnfocus();
-                }
+            if (nextWindow != null) {
+                nextWindow.setActive(true);
+                nextWindow.onFocus();
             }
-        }
+
+        } // synchronized (windows)
 
         // Perform window cleanup
         window.onClose();
@@ -2373,7 +2544,8 @@ public class TApplication implements Runnable {
             synchronized (secondaryEventHandler) {
                 secondaryEventHandler.notify();
             }
-        }
+
+        } // synchronized (windows)
 
         // Permit desktop to be active if it is the only thing left.
         if (desktop != null) {
@@ -2394,53 +2566,50 @@ public class TApplication implements Runnable {
         if (shownWindowCount() < 2) {
             return;
         }
-        assert (activeWindow != null);
+
+        if (modalWindowActive()) {
+            // Do not switch if a window is modal
+            return;
+        }
 
         synchronized (windows) {
-            // Whatever window might be moving/dragging, stop it now.
-            for (TWindow w: windows) {
-                if (w.inMovements()) {
-                    w.stopMovements();
-                }
-            }
 
-            // Swap z/active between active window and the next in the list
-            int activeWindowI = -1;
-            for (int i = 0; i < windows.size(); i++) {
-                if (windows.get(i) == activeWindow) {
-                    assert (activeWindow.isActive());
-                    activeWindowI = i;
-                    break;
+            TWindow window = windows.get(0);
+            do {
+                assert (window != null);
+                if (forward) {
+                    window.setZ(windows.size());
                 } else {
-                    assert (!windows.get(0).isActive());
+                    TWindow lastWindow = windows.get(windows.size() - 1);
+                    lastWindow.setZ(-1);
                 }
-            }
-            assert (activeWindowI >= 0);
-
-            // Do not switch if a window is modal
-            if (activeWindow.isModal()) {
-                return;
-            }
 
-            int nextWindowI = activeWindowI;
-            for (;;) {
-                if (forward) {
-                    nextWindowI++;
-                    nextWindowI %= windows.size();
-                } else {
-                    nextWindowI--;
-                    if (nextWindowI < 0) {
-                        nextWindowI = windows.size() - 1;
-                    }
+                Collections.sort(windows);
+                int newZ = 0;
+                for (TWindow w: windows) {
+                    w.setZ(newZ);
+                    newZ++;
                 }
 
-                if (windows.get(nextWindowI).isShown()) {
-                    activateWindow(windows.get(nextWindowI));
-                    break;
+                window = windows.get(0);
+            } while (!window.isShown());
+
+            // The next visible window is now on top.  Renumber the list.
+            for (TWindow w: windows) {
+                w.stopMovements();
+                if ((w != window) && w.isActive()) {
+                    assert (w.isShown());
+                    w.setActive(false);
+                    w.onUnfocus();
                 }
             }
-        } // synchronized (windows)
 
+            // Next visible window is on top.
+            assert (window.isShown());
+            window.setActive(true);
+            window.onFocus();
+
+        } // synchronized (windows)
     }
 
     /**
@@ -2490,13 +2659,13 @@ public class TApplication implements Runnable {
                     }
                     w.setZ(w.getZ() + 1);
                 }
-            }
-            windows.add(window);
-            if (window.isShown()) {
-                activeWindow = window;
-                activeWindow.setZ(0);
-                activeWindow.setActive(true);
-                activeWindow.onFocus();
+                window.setZ(0);
+                window.setActive(true);
+                window.onFocus();
+                windows.add(0, window);
+            } else {
+                window.setZ(windows.size());
+                windows.add(window);
             }
 
             if (((window.flags & TWindow.CENTERED) == 0)
@@ -2513,6 +2682,7 @@ public class TApplication implements Runnable {
         if (desktop != null) {
             desktop.setActive(false);
         }
+
     }
 
     /**
@@ -2540,6 +2710,7 @@ public class TApplication implements Runnable {
      * @return true if the active window is overriding the menu
      */
     private boolean overrideMenuWindowActive() {
+        TWindow activeWindow = getActiveWindow();
         if (activeWindow != null) {
             if (activeWindow.hasOverriddenMenu()) {
                 return true;
@@ -2910,7 +3081,6 @@ public class TApplication implements Runnable {
             || (mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
         ) {
             synchronized (windows) {
-                Collections.sort(windows);
                 if (windows.get(0).isModal()) {
                     // Modal windows don't switch
                     return;
@@ -2925,25 +3095,7 @@ public class TApplication implements Runnable {
                     }
 
                     if (window.mouseWouldHit(mouse)) {
-                        if (window == windows.get(0)) {
-                            // Clicked on the same window, nothing to do
-                            assert (window.isActive());
-                            return;
-                        }
-
-                        // We will be switching to another window
-                        assert (windows.get(0).isActive());
-                        assert (windows.get(0) == activeWindow);
-                        assert (!window.isActive());
-                        if (activeWindow != null) {
-                            activeWindow.onUnfocus();
-                            activeWindow.setActive(false);
-                            activeWindow.setZ(window.getZ());
-                        }
-                        activeWindow = window;
-                        window.setZ(0);
-                        window.setActive(true);
-                        window.onFocus();
+                        activateWindow(window);
                         return;
                     }
                 }
@@ -3278,10 +3430,13 @@ public class TApplication implements Runnable {
      */
     public final TMenu addEditMenu() {
         TMenu editMenu = addMenu(i18n.getString("editMenuTitle"));
-        editMenu.addDefaultItem(TMenu.MID_CUT);
-        editMenu.addDefaultItem(TMenu.MID_COPY);
-        editMenu.addDefaultItem(TMenu.MID_PASTE);
-        editMenu.addDefaultItem(TMenu.MID_CLEAR);
+        editMenu.addDefaultItem(TMenu.MID_UNDO, false);
+        editMenu.addDefaultItem(TMenu.MID_REDO, false);
+        editMenu.addSeparator();
+        editMenu.addDefaultItem(TMenu.MID_CUT, false);
+        editMenu.addDefaultItem(TMenu.MID_COPY, false);
+        editMenu.addDefaultItem(TMenu.MID_PASTE, false);
+        editMenu.addDefaultItem(TMenu.MID_CLEAR, false);
         TStatusBar statusBar = editMenu.newStatusBar(i18n.
             getString("editMenuStatus"));
         statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
similarity index 87%
rename from src/jexer/TApplication.properties
rename to TApplication.properties
index 299c6a3a90501469469dc7a8550a3c8f42cf146d..57f7c595d85e030d7ab56ce724a3817a4d8f2260 100644 (file)
@@ -25,3 +25,6 @@ exitDialogText=Exit application?
 
 aboutDialogTitle=About
 aboutDialogText=Jexer Version {0}
+
+searchHelpInputBoxTitle=Search Help Topics
+searchHelpInputBoxCaption=Search help topics for (regex):
similarity index 97%
rename from src/jexer/TButton.java
rename to TButton.java
index d86fa4400d16b51dc3be15dbaa38f5421ffdc7b5..d1d7b390cca0170e0c33f9cde044e968c64fc947 100644 (file)
@@ -129,6 +129,20 @@ public class TButton extends TWidget {
         this(parent, text, x, y);
         this.action = action;
     }
+    
+    /**
+     * The action to call when the button is pressed.
+     **/
+    public TAction getAction() {
+               return action;
+       }
+    
+    /**
+     * The action to call when the button is pressed.
+     **/
+    public void setAction(TAction action) {
+               this.action = action;
+       }
 
     // ------------------------------------------------------------------------
     // Event handlers ---------------------------------------------------------
similarity index 100%
rename from src/jexer/TCalendar.java
rename to TCalendar.java
similarity index 100%
rename from src/jexer/TCheckBox.java
rename to TCheckBox.java
similarity index 100%
rename from src/jexer/TComboBox.java
rename to TComboBox.java
similarity index 100%
rename from src/jexer/TCommand.java
rename to TCommand.java
similarity index 100%
rename from src/jexer/TDesktop.java
rename to TDesktop.java
diff --git a/TEditorWidget.java b/TEditorWidget.java
new file mode 100644 (file)
index 0000000..bea25ed
--- /dev/null
@@ -0,0 +1,1441 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import jexer.bits.CellAttributes;
+import jexer.bits.StringUtils;
+import jexer.event.TCommandEvent;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
+import jexer.event.TResizeEvent;
+import jexer.teditor.Document;
+import jexer.teditor.Line;
+import jexer.teditor.Word;
+import static jexer.TCommand.*;
+import static jexer.TKeypress.*;
+
+/**
+ * TEditorWidget displays an editable text document.  It is unaware of
+ * scrolling behavior, but can respond to mouse and keyboard events.
+ */
+public class TEditorWidget extends TWidget implements EditMenuUser {
+
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The number of lines to scroll on mouse wheel up/down.
+     */
+    private static final int wheelScrollSize = 3;
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The document being edited.
+     */
+    protected Document document;
+
+    /**
+     * The default color for the editable text.
+     */
+    private CellAttributes defaultColor = null;
+
+    /**
+     * The topmost line number in the visible area.  0-based.
+     */
+    private int topLine = 0;
+
+    /**
+     * The leftmost column number in the visible area.  0-based.
+     */
+    private int leftColumn = 0;
+
+    /**
+     * If true, the mouse is dragging a selection.
+     */
+    private boolean inSelection = false;
+
+    /**
+     * Selection starting column.
+     */
+    private int selectionColumn0;
+
+    /**
+     * Selection starting line.
+     */
+    private int selectionLine0;
+
+    /**
+     * Selection ending column.
+     */
+    private int selectionColumn1;
+
+    /**
+     * Selection ending line.
+     */
+    private int selectionLine1;
+
+    /**
+     * The list of undo/redo states.
+     */
+    private List<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);
+    }
+
+}
similarity index 92%
rename from src/jexer/TEditorWindow.java
rename to TEditorWindow.java
index d78185c32f3096cd615f345d9731751d285fc3b6..a28376ba3a18e8fcdfd75a300a32c180c2946385 100644 (file)
@@ -44,8 +44,10 @@ import jexer.bits.CellAttributes;
 import jexer.bits.GraphicsChars;
 import jexer.event.TCommandEvent;
 import jexer.event.TKeypressEvent;
+import jexer.event.TMenuEvent;
 import jexer.event.TMouseEvent;
 import jexer.event.TResizeEvent;
+import jexer.menu.TMenu;
 import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
@@ -150,28 +152,27 @@ public class TEditorWindow extends TScrollableWindow {
     }
 
     // ------------------------------------------------------------------------
-    // TWindow ----------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
     // ------------------------------------------------------------------------
 
     /**
-     * Draw the window.
+     * Called by application.switchWindow() when this window gets the
+     * focus, and also by application.addWindow().
      */
-    @Override
-    public void draw() {
-        // Draw as normal.
-        super.draw();
-
-        // Add the row:col on the bottom row
-        CellAttributes borderColor = getBorder();
-        String location = String.format(" %d:%d ",
-            editField.getEditingRowNumber(),
-            editField.getEditingColumnNumber());
-        int colon = location.indexOf(':');
-        putStringXY(10 - colon, getHeight() - 1, location, borderColor);
+    public void onFocus() {
+        super.onFocus();
+        getApplication().enableMenuItem(TMenu.MID_UNDO);
+        getApplication().enableMenuItem(TMenu.MID_REDO);
+    }
 
-        if (editField.isDirty()) {
-            putCharXY(2, getHeight() - 1, GraphicsChars.OCTOSTAR, borderColor);
-        }
+    /**
+     * Called by application.switchWindow() when another window gets the
+     * focus.
+     */
+    public void onUnfocus() {
+        super.onUnfocus();
+        getApplication().disableMenuItem(TMenu.MID_UNDO);
+        getApplication().disableMenuItem(TMenu.MID_REDO);
     }
 
     /**
@@ -351,6 +352,48 @@ public class TEditorWindow extends TScrollableWindow {
         super.onCommand(command);
     }
 
+    /**
+     * Handle posted menu events.
+     *
+     * @param menu menu event
+     */
+    @Override
+    public void onMenu(final TMenuEvent menu) {
+        switch (menu.getId()) {
+        case TMenu.MID_UNDO:
+            editField.undo();
+            break;
+        case TMenu.MID_REDO:
+            editField.redo();
+            break;
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // TWindow ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw the window.
+     */
+    @Override
+    public void draw() {
+        // Draw as normal.
+        super.draw();
+
+        // Add the row:col on the bottom row
+        CellAttributes borderColor = getBorder();
+        String location = String.format(" %d:%d ",
+            editField.getEditingRowNumber(),
+            editField.getEditingColumnNumber());
+        int colon = location.indexOf(':');
+        putStringXY(10 - colon, getHeight() - 1, location, borderColor);
+
+        if (editField.isDirty()) {
+            putCharXY(2, getHeight() - 1, GraphicsChars.OCTOSTAR, borderColor);
+        }
+    }
+
     /**
      * Returns true if this window does not want the application-wide mouse
      * cursor drawn over it.
similarity index 97%
rename from src/jexer/TExceptionDialog.java
rename to TExceptionDialog.java
index 227aceb5764b92d26a0cac983030a0def359a402..f526a6470a2db15634fdeaeb1d09d22c8ca16589 100644 (file)
@@ -82,7 +82,7 @@ public class TExceptionDialog extends TWindow {
         final Throwable exception) {
 
         super(application, i18n.getString("windowTitle"),
-            1, 1, 70, 20, CENTERED | MODAL);
+            1, 1, 78, 22, CENTERED | MODAL);
 
         this.exception = exception;
 
@@ -100,14 +100,15 @@ public class TExceptionDialog extends TWindow {
             2, 6, "ttext", false);
 
         ArrayList<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();
@@ -115,7 +116,7 @@ public class TExceptionDialog extends TWindow {
             });
 
         TButton closeButton = addButton(i18n.getString("closeButton"),
-            35, getHeight() - 4,
+            37, getHeight() - 4,
             new TAction() {
                 public void DO() {
                     // Don't do anything, just close the window.
similarity index 52%
rename from src/jexer/TExceptionDialog.properties
rename to TExceptionDialog.properties
index d07998cf2931c6956becc0d2a177f90d5a153d8c..9e5857a6deff14c2374b09c41ce5d1fc2362469d 100644 (file)
@@ -1,10 +1,10 @@
 windowTitle=Java Exception Caught
 statusBar=Exception
 
-captionLine1=An error has occurred.  This may be due to a programming bug, but
-captionLine2=could also be a correctable or temporary issue.  The stack trace
-captionLine3=is reported below.  If you wish to submit a bug report, please
-captionLine4=use the Save button to create a more detailed error log.
+captionLine1=An error has occurred.  This may be due to a programming bug, but could
+captionLine2=also be a correctable or temporary issue.  The stack trace is reported
+captionLine3=below.  If you wish to submit a bug report, please use the Save button
+captionLine4=to create a more detailed error log.
 
 exceptionString={0}: {1}
 
similarity index 90%
rename from src/jexer/TField.java
rename to TField.java
index 7c8b5bc415e62882a24941734da6ac213c706b75..90dd4e427abcf5660bfcc04b32b76c7c5da8edf7 100644 (file)
@@ -31,14 +31,16 @@ package jexer;
 import jexer.bits.CellAttributes;
 import jexer.bits.GraphicsChars;
 import jexer.bits.StringUtils;
+import jexer.event.TCommandEvent;
 import jexer.event.TKeypressEvent;
 import jexer.event.TMouseEvent;
+import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
 /**
  * TField implements an editable text field.
  */
-public class TField extends TWidget {
+public class TField extends TWidget implements EditMenuUser {
 
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
@@ -234,12 +236,13 @@ public class TField extends TWidget {
 
         if (keypress.equals(kbRight)) {
             if (position < text.length()) {
+                int lastPosition = position;
                 screenPosition += StringUtils.width(text.codePointAt(position));
                 position += Character.charCount(text.codePointAt(position));
                 if (fixed == true) {
                     if (screenPosition == getWidth()) {
                         screenPosition--;
-                        position -= Character.charCount(text.codePointAt(position));
+                        position -= Character.charCount(text.codePointAt(lastPosition));
                     }
                 } else {
                     while ((screenPosition - windowStart +
@@ -374,6 +377,43 @@ public class TField extends TWidget {
         super.onKeypress(keypress);
     }
 
+    /**
+     * Handle posted command events.
+     *
+     * @param command command event
+     */
+    @Override
+    public void onCommand(final TCommandEvent command) {
+        if (command.equals(cmCut)) {
+            // Copy text to clipboard, and then remove it.
+            getClipboard().copyText(text);
+            setText("");
+            return;
+        }
+
+        if (command.equals(cmCopy)) {
+            // Copy text to clipboard.
+            getClipboard().copyText(text);
+            return;
+        }
+
+        if (command.equals(cmPaste)) {
+            // Paste text from clipboard.
+            String newText = getClipboard().pasteText();
+            if (newText != null) {
+                setText(newText);
+            }
+            return;
+        }
+
+        if (command.equals(cmClear)) {
+            // Remove text.
+            setText("");
+            return;
+        }
+
+    }
+
     // ------------------------------------------------------------------------
     // TWidget ----------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -467,7 +507,11 @@ public class TField extends TWidget {
         assert (text != null);
         this.text = text;
         position = 0;
+        screenPosition = 0;
         windowStart = 0;
+        if ((fixed == true) && (this.text.length() > getWidth())) {
+            this.text = this.text.substring(0, getWidth());
+        }
     }
 
     /**
@@ -668,4 +712,44 @@ public class TField extends TWidget {
         updateAction = action;
     }
 
+    // ------------------------------------------------------------------------
+    // EditMenuUser -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Check if the cut menu item should be enabled.
+     *
+     * @return true if the cut menu item should be enabled
+     */
+    public boolean isEditMenuCut() {
+        return true;
+    }
+
+    /**
+     * Check if the copy menu item should be enabled.
+     *
+     * @return true if the copy menu item should be enabled
+     */
+    public boolean isEditMenuCopy() {
+        return true;
+    }
+
+    /**
+     * Check if the paste menu item should be enabled.
+     *
+     * @return true if the paste menu item should be enabled
+     */
+    public boolean isEditMenuPaste() {
+        return true;
+    }
+
+    /**
+     * Check if the clear menu item should be enabled.
+     *
+     * @return true if the clear menu item should be enabled
+     */
+    public boolean isEditMenuClear() {
+        return true;
+    }
+
 }
similarity index 100%
rename from src/jexer/THScroller.java
rename to THScroller.java
diff --git a/THelpWindow.java b/THelpWindow.java
new file mode 100644 (file)
index 0000000..ee7ce54
--- /dev/null
@@ -0,0 +1,271 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+import java.util.ResourceBundle;
+
+import jexer.bits.CellAttributes;
+import jexer.event.TResizeEvent;
+import jexer.help.THelpText;
+import jexer.help.Topic;
+
+/**
+ * THelpWindow
+ */
+public class THelpWindow extends TWindow {
+
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(THelpWindow.class.getName());
+
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    // Default help topic keys.  Note package private access.
+    static String HELP_HELP                     = "Help On Help";
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The help text window.
+     */
+    private THelpText helpText;
+
+    /**
+     * The "Contents" button.
+     */
+    private TButton contentsButton;
+
+    /**
+     * The "Index" button.
+     */
+    private TButton indexButton;
+
+    /**
+     * The "Previous" button.
+     */
+    private TButton previousButton;
+
+    /**
+     * The "Close" button.
+     */
+    private TButton closeButton;
+
+    /**
+     * The X position for the buttons.
+     */
+    private int buttonOffset = 14;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param application TApplication that manages this window
+     * @param topic the topic to start on
+     */
+    public THelpWindow(final TApplication application, final String topic) {
+        this (application, application.helpFile.getTopic(topic));
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param application TApplication that manages this window
+     * @param topic the topic to start on
+     */
+    public THelpWindow(final TApplication application, final Topic topic) {
+        super(application, i18n.getString("windowTitle"),
+            1, 1, 78, 22, CENTERED | RESIZABLE);
+
+        setMinimumWindowHeight(16);
+        setMinimumWindowWidth(30);
+
+        helpText = new THelpText(this, topic, 1, 1,
+            getWidth() - buttonOffset - 4, getHeight() - 4);
+
+        setHelpTopic(topic);
+
+        // Buttons
+        previousButton = addButton(i18n.getString("previousButton"),
+            getWidth() - buttonOffset, 4,
+            new TAction() {
+                public void DO() {
+                    if (application.helpTopics.size() > 1) {
+                        Topic previous = application.helpTopics.remove(
+                            application.helpTopics.size() - 2);
+                        application.helpTopics.remove(application.
+                            helpTopics.size() - 1);
+                        setHelpTopic(previous);
+                    }
+                }
+            });
+
+        contentsButton = addButton(i18n.getString("contentsButton"),
+            getWidth() - buttonOffset, 6,
+            new TAction() {
+                public void DO() {
+                    setHelpTopic(application.helpFile.getTableOfContents());
+                }
+            });
+
+        indexButton = addButton(i18n.getString("indexButton"),
+            getWidth() - buttonOffset, 8,
+            new TAction() {
+                public void DO() {
+                    setHelpTopic(application.helpFile.getIndex());
+                }
+            });
+
+        closeButton = addButton(i18n.getString("closeButton"),
+            getWidth() - buttonOffset, 10,
+            new TAction() {
+                public void DO() {
+                    // Don't copy anything, just close the window.
+                    THelpWindow.this.close();
+                }
+            });
+
+        // Save this for last: make the close button default action.
+        activate(closeButton);
+
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param application TApplication that manages this window
+     */
+    public THelpWindow(final TApplication application) {
+        this(application, HELP_HELP);
+    }
+
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Handle window/screen resize events.
+     *
+     * @param event resize event
+     */
+    @Override
+    public void onResize(final TResizeEvent event) {
+        if (event.getType() == TResizeEvent.Type.WIDGET) {
+
+            previousButton.setX(getWidth() - buttonOffset);
+            contentsButton.setX(getWidth() - buttonOffset);
+            indexButton.setX(getWidth() - buttonOffset);
+            closeButton.setX(getWidth() - buttonOffset);
+
+            helpText.setDimensions(1, 1, getWidth() - buttonOffset - 4,
+                getHeight() - 4);
+            helpText.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+                    helpText.getWidth(), helpText.getHeight()));
+
+            return;
+        } else {
+            super.onResize(event);
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // TWindow ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Retrieve the background color.
+     *
+     * @return the background color
+     */
+    @Override
+    public final CellAttributes getBackground() {
+        return getTheme().getColor("thelpwindow.background");
+    }
+
+    /**
+     * Retrieve the border color.
+     *
+     * @return the border color
+     */
+    @Override
+    public CellAttributes getBorder() {
+        if (inWindowMove) {
+            return getTheme().getColor("thelpwindow.windowmove");
+        }
+        return getTheme().getColor("thelpwindow.background");
+    }
+
+    /**
+     * Retrieve the color used by the window movement/sizing controls.
+     *
+     * @return the color used by the zoom box, resize bar, and close box
+     */
+    @Override
+    public CellAttributes getBorderControls() {
+        return getTheme().getColor("thelpwindow.border");
+    }
+
+    // ------------------------------------------------------------------------
+    // THelpWindow ------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Set the topic to display.
+     *
+     * @param topic the topic to display
+     */
+    public void setHelpTopic(final String topic) {
+        setHelpTopic(getApplication().helpFile.getTopic(topic));
+    }
+
+    /**
+     * Set the topic to display.
+     *
+     * @param topic the topic to display
+     */
+    private void setHelpTopic(final Topic topic) {
+        boolean separator = true;
+        if ((topic == getApplication().helpFile.getTableOfContents())
+            || (topic == getApplication().helpFile.getIndex())
+        ) {
+            separator = false;
+        }
+
+        getApplication().helpTopics.add(topic);
+        helpText.setTopic(topic, separator);
+    }
+
+}
diff --git a/THelpWindow.properties b/THelpWindow.properties
new file mode 100644 (file)
index 0000000..2b25484
--- /dev/null
@@ -0,0 +1,5 @@
+windowTitle=Help
+previousButton=Pre&vious
+contentsButton=Co&ntents
+indexButton=\ &Index\ \ 
+closeButton=\ C&lose\ \ 
similarity index 90%
rename from src/jexer/TImage.java
rename to TImage.java
index cd0ce96e0baf4523c64cb45527c01cbc4d1e1443..b7bfbd00a2ff165becc701575023b42e50b6deed 100644 (file)
@@ -30,19 +30,18 @@ package jexer;
 
 import java.awt.image.BufferedImage;
 
-import jexer.backend.ECMA48Terminal;
-import jexer.backend.MultiScreen;
-import jexer.backend.SwingTerminal;
 import jexer.bits.Cell;
+import jexer.event.TCommandEvent;
 import jexer.event.TKeypressEvent;
 import jexer.event.TMouseEvent;
 import jexer.event.TResizeEvent;
+import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
 /**
  * TImage renders a piece of a bitmap image on screen.
  */
-public class TImage extends TWidget {
+public class TImage extends TWidget implements EditMenuUser {
 
     // ------------------------------------------------------------------------
     // Constants --------------------------------------------------------------
@@ -325,6 +324,20 @@ public class TImage extends TWidget {
         resized = true;
     }
 
+    /**
+     * Handle posted command events.
+     *
+     * @param command command event
+     */
+    @Override
+    public void onCommand(final TCommandEvent command) {
+        if (command.equals(cmCopy)) {
+            // Copy image to clipboard.
+            getClipboard().copyImage(image);
+            return;
+        }
+    }
+
     // ------------------------------------------------------------------------
     // TWidget ----------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -409,8 +422,21 @@ public class TImage extends TWidget {
                     }
 
                     Cell cell = new Cell();
-                    cell.setImage(image.getSubimage(x * textWidth,
-                            y * textHeight, width, height));
+                    if ((width != textWidth) || (height != textHeight)) {
+                        BufferedImage newImage;
+                        newImage = new BufferedImage(textWidth, textHeight,
+                            BufferedImage.TYPE_INT_ARGB);
+
+                        java.awt.Graphics gr = newImage.getGraphics();
+                        gr.drawImage(image.getSubimage(x * textWidth,
+                                y * textHeight, width, height),
+                            0, 0, null, null);
+                        gr.dispose();
+                        cell.setImage(newImage);
+                    } else {
+                        cell.setImage(image.getSubimage(x * textWidth,
+                                y * textHeight, width, height));
+                    }
 
                     cells[x][y] = cell;
                 }
@@ -762,4 +788,44 @@ public class TImage extends TWidget {
         return newImage;
     }
 
+    // ------------------------------------------------------------------------
+    // EditMenuUser -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Check if the cut menu item should be enabled.
+     *
+     * @return true if the cut menu item should be enabled
+     */
+    public boolean isEditMenuCut() {
+        return false;
+    }
+
+    /**
+     * Check if the copy menu item should be enabled.
+     *
+     * @return true if the copy menu item should be enabled
+     */
+    public boolean isEditMenuCopy() {
+        return true;
+    }
+
+    /**
+     * Check if the paste menu item should be enabled.
+     *
+     * @return true if the paste menu item should be enabled
+     */
+    public boolean isEditMenuPaste() {
+        return false;
+    }
+
+    /**
+     * Check if the clear menu item should be enabled.
+     *
+     * @return true if the clear menu item should be enabled
+     */
+    public boolean isEditMenuClear() {
+        return false;
+    }
+
 }
similarity index 100%
rename from src/jexer/TInputBox.java
rename to TInputBox.java
similarity index 95%
rename from src/jexer/TKeypress.java
rename to TKeypress.java
index c965e7dbab48873ae31a35963d4aaef4231cdcaf..20db8bb267ebc43389f2d900c49aa5b3462d22d1 100644 (file)
@@ -612,6 +612,41 @@ public class TKeypress {
     public static final TKeypress kbAltShiftZ = new TKeypress(false,
             0, 'Z', true, false, true);
 
+    public static final TKeypress kbAltShiftHome = new TKeypress(true,
+            TKeypress.HOME, ' ', true, false, true);
+    public static final TKeypress kbAltShiftEnd = new TKeypress(true,
+            TKeypress.END, ' ', true, false, true);
+    public static final TKeypress kbAltShiftPgUp = new TKeypress(true,
+            TKeypress.PGUP, ' ', true, false, true);
+    public static final TKeypress kbAltShiftPgDn = new TKeypress(true,
+            TKeypress.PGDN, ' ', true, false, true);
+    public static final TKeypress kbAltShiftUp = new TKeypress(true,
+            TKeypress.UP, ' ', true, false, true);
+    public static final TKeypress kbAltShiftDown = new TKeypress(true,
+            TKeypress.DOWN, ' ', true, false, true);
+    public static final TKeypress kbAltShiftLeft = new TKeypress(true,
+            TKeypress.LEFT, ' ', true, false, true);
+    public static final TKeypress kbAltShiftRight = new TKeypress(true,
+            TKeypress.RIGHT, ' ', true, false, true);
+
+    public static final TKeypress kbCtrlShiftHome = new TKeypress(true,
+            TKeypress.HOME, ' ', false, true, true);
+    public static final TKeypress kbCtrlShiftEnd = new TKeypress(true,
+            TKeypress.END, ' ', false, true, true);
+    public static final TKeypress kbCtrlShiftPgUp = new TKeypress(true,
+            TKeypress.PGUP, ' ', false, true, true);
+    public static final TKeypress kbCtrlShiftPgDn = new TKeypress(true,
+            TKeypress.PGDN, ' ', false, true, true);
+    public static final TKeypress kbCtrlShiftUp = new TKeypress(true,
+            TKeypress.UP, ' ', false, true, true);
+    public static final TKeypress kbCtrlShiftDown = new TKeypress(true,
+            TKeypress.DOWN, ' ', false, true, true);
+    public static final TKeypress kbCtrlShiftLeft = new TKeypress(true,
+            TKeypress.LEFT, ' ', false, true, true);
+    public static final TKeypress kbCtrlShiftRight = new TKeypress(true,
+            TKeypress.RIGHT, ' ', false, true, true);
+
+
     /**
      * Backspace as ^H.
      */
@@ -821,6 +856,11 @@ public class TKeypress {
             return "\u25C0\u2500\u2518";
         }
 
+        // Special case: Space is "Space"
+        if (equals(kbSpace)) {
+            return "Space";
+        }
+
         if (equals(kbShiftLeft)) {
             return "Shift+\u2190";
         }
similarity index 100%
rename from src/jexer/TLabel.java
rename to TLabel.java
similarity index 97%
rename from src/jexer/TList.java
rename to TList.java
index 38a994c8215bbba2a53e3c85d0b15bd46c099146..12e0b8a33cce977e93ce4a6fd30080adeb75e8dc 100644 (file)
@@ -335,21 +335,28 @@ public class TList extends TScrollableWidget {
     @Override
     public void setWidth(final int width) {
         super.setWidth(width);
-        hScroller.setWidth(getWidth() - 1);
-        vScroller.setX(getWidth() - 1);
+        if (hScroller != null) {
+            hScroller.setWidth(getWidth() - 1);
+        }
+        if (vScroller != null) {
+            vScroller.setX(getWidth() - 1);
+        }
     }
 
     /**
      * Override TWidget's height: we need to set child widget heights.
-     * time.
      *
      * @param height new widget height
      */
     @Override
     public void setHeight(final int height) {
         super.setHeight(height);
-        hScroller.setY(getHeight() - 1);
-        vScroller.setHeight(getHeight() - 1);
+        if (hScroller != null) {
+            hScroller.setY(getHeight() - 1);
+        }
+        if (vScroller != null) {
+            vScroller.setHeight(getHeight() - 1);
+        }
     }
 
     /**
@@ -391,6 +398,9 @@ public class TList extends TScrollableWidget {
         int topY = 0;
         for (int i = begin; i < strings.size(); i++) {
             String line = strings.get(i);
+            if (line == null) {
+                line = "";
+            }
             if (getHorizontalValue() < line.length()) {
                 line = line.substring(getHorizontalValue());
             } else {
similarity index 100%
rename from src/jexer/TMessageBox.java
rename to TMessageBox.java
similarity index 100%
rename from src/jexer/TPanel.java
rename to TPanel.java
similarity index 99%
rename from src/jexer/TPasswordField.java
rename to TPasswordField.java
index 9c200d7dc7dcfea44a2fef0d3f59f8df9505ec81..0be2b98ce3ff73e0a9668b621f2639df791f0c1d 100644 (file)
@@ -29,7 +29,6 @@
 package jexer;
 
 import jexer.bits.CellAttributes;
-import jexer.bits.GraphicsChars;
 import jexer.bits.StringUtils;
 
 /**
similarity index 91%
rename from src/jexer/TRadioButton.java
rename to TRadioButton.java
index 60a628845ca2c5c920863bd31be7597c95709266..dcc5c13699f03fefceabbc54bf65bac1b6624745 100644 (file)
@@ -50,9 +50,9 @@ public class TRadioButton extends TWidget {
     // ------------------------------------------------------------------------
 
     /**
-     * RadioButton state, true means selected.
+     * RadioButton state, true means selected.  Note package private access.
      */
-    private boolean selected = false;
+    boolean selected = false;
 
     /**
      * The shortcut and radio button label.
@@ -61,16 +61,16 @@ public class TRadioButton extends TWidget {
 
     /**
      * ID for this radio button.  Buttons start counting at 1 in the
-     * RadioGroup.
+     * RadioGroup.  Note package private access.
      */
-    private int id;
+    int id;
 
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
 
     /**
-     * Public constructor.
+     * Package private constructor.
      *
      * @param parent parent widget
      * @param x column relative to parent
@@ -78,7 +78,7 @@ public class TRadioButton extends TWidget {
      * @param label label to display next to (right of) the radiobutton
      * @param id ID for this radio button
      */
-    public TRadioButton(final TRadioGroup parent, final int x, final int y,
+    TRadioButton(final TRadioGroup parent, final int x, final int y,
         final String label, final int id) {
 
         // Set parent and window
@@ -89,6 +89,8 @@ public class TRadioButton extends TWidget {
 
         setCursorVisible(true);
         setCursorX(1);
+
+        parent.addRadioButton(this);
     }
 
     // ------------------------------------------------------------------------
@@ -120,8 +122,7 @@ public class TRadioButton extends TWidget {
     public void onMouseDown(final TMouseEvent mouse) {
         if ((mouseOnRadioButton(mouse)) && (mouse.isMouse1())) {
             // Switch state
-            selected = true;
-            ((TRadioGroup) getParent()).setSelected(this);
+            ((TRadioGroup) getParent()).setSelected(id);
         }
     }
 
@@ -134,8 +135,7 @@ public class TRadioButton extends TWidget {
     public void onKeypress(final TKeypressEvent keypress) {
 
         if (keypress.equals(kbSpace)) {
-            selected = true;
-            ((TRadioGroup) getParent()).setSelected(this);
+            ((TRadioGroup) getParent()).setSelected(id);
             return;
         }
 
@@ -222,14 +222,17 @@ public class TRadioButton extends TWidget {
     }
 
     /**
-     * Set RadioButton state, true means selected.  Note package private
-     * access.
+     * Set RadioButton state, true means selected.
      *
      * @param selected if true then this is the one button in the group that
      * is selected
      */
-    void setSelected(final boolean selected) {
-        this.selected = selected;
+    public void setSelected(final boolean selected) {
+        if (selected == true) {
+            ((TRadioGroup) getParent()).setSelected(id);
+        } else {
+            ((TRadioGroup) getParent()).setSelected(0);
+        }
     }
 
     /**
similarity index 69%
rename from src/jexer/TRadioGroup.java
rename to TRadioGroup.java
index a82b074f8ce9a1c4fe6de462433b8d4124507b91..d6bd7ff38ee34c2b71a3546fb1cf1347e29c1dfe 100644 (file)
@@ -54,12 +54,30 @@ public class TRadioGroup extends TWidget {
      * If true, one of the children MUST be selected.  Note package private
      * access.
      */
-    boolean requiresSelection = true;
+    boolean requiresSelection = false;
 
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of group
+     * @param label label to display on the group box
+     */
+    public TRadioGroup(final TWidget parent, final int x, final int y,
+        final int width, final String label) {
+
+        // Set parent and window
+        super(parent, x, y, width, 2);
+
+        this.label = label;
+    }
+
     /**
      * Public constructor.
      *
@@ -138,19 +156,6 @@ public class TRadioGroup extends TWidget {
         return selectedButton.getId();
     }
 
-    /**
-     * Set the new selected radio button.  Note package private access.
-     *
-     * @param button new button that became selected
-     */
-    void setSelected(final TRadioButton button) {
-        assert (button.isSelected());
-        if ((selectedButton != null) && (selectedButton != button)) {
-            selectedButton.setSelected(false);
-        }
-        selectedButton = button;
-    }
-
     /**
      * Set the new selected radio button.  1-based.
      *
@@ -161,19 +166,43 @@ public class TRadioGroup extends TWidget {
             return;
         }
 
+        for (TWidget widget: getChildren()) {
+            ((TRadioButton) widget).selected = false;
+        }
         if (id == 0) {
-            for (TWidget widget: getChildren()) {
-                ((TRadioButton) widget).setSelected(false);
-            }
             selectedButton = null;
             return;
         }
         assert ((id > 0) && (id <= getChildren().size()));
         TRadioButton button = (TRadioButton) (getChildren().get(id - 1));
-        button.setSelected(true);
+        button.selected = true;
         selectedButton = button;
     }
 
+    /**
+     * Get the radio button that was selected.
+     *
+     * @return the selected button, or null if no button is selected
+     */
+    public TRadioButton getSelectedButton() {
+        return selectedButton;
+    }
+
+    /**
+     * Convenience function to add a radio button to this group.
+     *
+     * @param label label to display next to (right of) the radiobutton
+     * @param selected if true, this will be the selected radiobutton
+     * @return the new radio button
+     */
+    public TRadioButton addRadioButton(final String label,
+        final boolean selected) {
+
+        TRadioButton button = addRadioButton(label);
+        setSelected(button.id);
+        return button;
+    }
+
     /**
      * Convenience function to add a radio button to this group.
      *
@@ -181,14 +210,25 @@ public class TRadioGroup extends TWidget {
      * @return the new radio button
      */
     public TRadioButton addRadioButton(final String label) {
-        int buttonX = 1;
-        int buttonY = getChildren().size() + 1;
+        return new TRadioButton(this, 0, 0, label, 0);
+    }
+
+    /**
+     * Package private method for RadioButton to add itself to a RadioGroup
+     * container.
+     *
+     * @param button the button to add
+     */
+    void addRadioButton(final TRadioButton button) {
+        super.setHeight(getChildren().size() + 2);
+        button.setX(1);
+        button.setY(getChildren().size());
+        button.id = getChildren().size();
+        String label = button.getMnemonic().getRawLabel();
+
         if (StringUtils.width(label) + 4 > getWidth()) {
             super.setWidth(StringUtils.width(label) + 7);
         }
-        super.setHeight(getChildren().size() + 3);
-        TRadioButton button = new TRadioButton(this, buttonX, buttonY, label,
-            getChildren().size() + 1);
 
         if (getParent().getLayoutManager() != null) {
             getParent().getLayoutManager().resetSize(this);
@@ -196,8 +236,31 @@ public class TRadioGroup extends TWidget {
 
         // Default to the first item on the list.
         activate(getChildren().get(0));
+    }
 
-        return button;
+    /**
+     * Get the requires selection flag.
+     *
+     * @return true if this radiogroup requires that one of the buttons be
+     * selected
+     */
+    public boolean getRequiresSelection() {
+        return requiresSelection;
+    }
+
+    /**
+     * Set the requires selection flag.
+     *
+     * @param requiresSelection if true, then this radiogroup requires that
+     * one of the buttons be selected
+     */
+    public void setRequiresSelection(final boolean requiresSelection) {
+        this.requiresSelection = requiresSelection;
+        if (requiresSelection) {
+            if ((getChildren().size() > 0) && (selectedButton == null)) {
+                setSelected(1);
+            }
+        }
     }
 
 }
similarity index 100%
rename from src/jexer/TSpinner.java
rename to TSpinner.java
similarity index 90%
rename from src/jexer/TSplitPane.java
rename to TSplitPane.java
index 7c85278f88d0d6df3ff34f01064e2be57d25481d..b308e9b79162a57aea99d55bad2a77f22e951eb3 100644 (file)
@@ -30,10 +30,8 @@ package jexer;
 
 import jexer.bits.CellAttributes;
 import jexer.bits.GraphicsChars;
-import jexer.event.TMenuEvent;
 import jexer.event.TMouseEvent;
 import jexer.event.TResizeEvent;
-import jexer.menu.TMenu;
 
 /**
  * TSplitPane contains two widgets with a draggable horizontal or vertical
@@ -225,7 +223,28 @@ public class TSplitPane extends TWidget {
         CellAttributes attr = getTheme().getColor("tsplitpane");
         if (vertical) {
             vLineXY(split, 0, getHeight(), GraphicsChars.WINDOW_SIDE, attr);
-            // TODO: draw intersections of children
+
+            // Draw intersections of children
+            if ((left instanceof TSplitPane)
+                && (((TSplitPane) left).vertical == false)
+                && (right instanceof TSplitPane)
+                && (((TSplitPane) right).vertical == false)
+                && (((TSplitPane) left).split == ((TSplitPane) right).split)
+            ) {
+                putCharXY(split, ((TSplitPane) left).split, '\u253C', attr);
+            } else {
+                if ((left instanceof TSplitPane)
+                    && (((TSplitPane) left).vertical == false)
+                ) {
+                    putCharXY(split, ((TSplitPane) left).split, '\u2524', attr);
+                }
+                if ((right instanceof TSplitPane)
+                    && (((TSplitPane) right).vertical == false)
+                ) {
+                    putCharXY(split, ((TSplitPane) right).split, '\u251C',
+                        attr);
+                }
+            }
 
             if ((mouse != null)
                 && (mouse.getAbsoluteX() == getAbsoluteX() + split)
@@ -237,7 +256,28 @@ public class TSplitPane extends TWidget {
             }
         } else {
             hLineXY(0, split, getWidth(), GraphicsChars.SINGLE_BAR, attr);
-            // TODO: draw intersections of children
+
+            // Draw intersections of children
+            if ((top instanceof TSplitPane)
+                && (((TSplitPane) top).vertical == true)
+                && (bottom instanceof TSplitPane)
+                && (((TSplitPane) bottom).vertical == true)
+                && (((TSplitPane) top).split == ((TSplitPane) bottom).split)
+            ) {
+                putCharXY(((TSplitPane) top).split, split, '\u253C', attr);
+            } else {
+                if ((top instanceof TSplitPane)
+                    && (((TSplitPane) top).vertical == true)
+                ) {
+                    putCharXY(((TSplitPane) top).split, split, '\u2534', attr);
+                }
+                if ((bottom instanceof TSplitPane)
+                    && (((TSplitPane) bottom).vertical == true)
+                ) {
+                    putCharXY(((TSplitPane) bottom).split, split, '\u252C',
+                        attr);
+                }
+            }
 
             if ((mouse != null)
                 && (mouse.getAbsoluteY() == getAbsoluteY() + split)
@@ -595,7 +635,7 @@ public class TSplitPane extends TWidget {
             keep.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
                     getHeight()));
         }
-        
+
         return keep;
     }
 
similarity index 100%
rename from src/jexer/TStatusBar.java
rename to TStatusBar.java
similarity index 99%
rename from src/jexer/TTableWidget.java
rename to TTableWidget.java
index 9b4d7c9847faaa6688dae4a65b63173169683e77..749b7313c9e29874cacacc431305ebf81e6d70ce 100644 (file)
@@ -1426,8 +1426,6 @@ public class TTableWidget extends TWidget {
                 for (int i = 0; i < list.size(); i++) {
                     rows.get(selectedRow).get(i).setText(list.get(i));
                 }
-
-                // TODO: detect header line
             }
         } finally {
             if (reader != null) {
similarity index 99%
rename from src/jexer/TTableWindow.java
rename to TTableWindow.java
index 44ff7b48949749c1590bcead79b57f3b55d727fc..766ceafdd901a0a4044105aba5114093cf7ef7a6 100644 (file)
@@ -112,7 +112,6 @@ public class TTableWindow extends TScrollableWindow {
      */
     public void onFocus() {
         // Enable the table menu items.
-        getApplication().enableMenuItem(TMenu.MID_CUT);
         getApplication().enableMenuItem(TMenu.MID_TABLE_RENAME_COLUMN);
         getApplication().enableMenuItem(TMenu.MID_TABLE_RENAME_ROW);
         getApplication().enableMenuItem(TMenu.MID_TABLE_VIEW_ROW_LABELS);
@@ -171,7 +170,6 @@ public class TTableWindow extends TScrollableWindow {
      */
     public void onUnfocus() {
         // Disable the table menu items.
-        getApplication().disableMenuItem(TMenu.MID_CUT);
         getApplication().disableMenuItem(TMenu.MID_TABLE_RENAME_COLUMN);
         getApplication().disableMenuItem(TMenu.MID_TABLE_RENAME_ROW);
         getApplication().disableMenuItem(TMenu.MID_TABLE_VIEW_ROW_LABELS);
similarity index 85%
rename from src/jexer/TTerminalWidget.java
rename to TTerminalWidget.java
index a2696092ce82ee7ed0550a0019b6b8fc0c9c785d..bf51e6b5c2fd67b78b55d6e280e44f68d3b335aa 100644 (file)
  */
 package jexer;
 
-import java.awt.Font;
-import java.awt.FontMetrics;
 import java.awt.Graphics2D;
 import java.awt.image.BufferedImage;
-
-import java.io.InputStream;
+import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.Field;
 import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.ResourceBundle;
 
 import jexer.backend.ECMA48Terminal;
 import jexer.backend.GlyphMaker;
-import jexer.backend.MultiScreen;
 import jexer.backend.SwingTerminal;
 import jexer.bits.Cell;
-import jexer.bits.CellAttributes;
+import jexer.event.TCommandEvent;
 import jexer.event.TKeypressEvent;
 import jexer.event.TMenuEvent;
 import jexer.event.TMouseEvent;
@@ -57,13 +51,14 @@ import jexer.menu.TMenu;
 import jexer.tterminal.DisplayLine;
 import jexer.tterminal.DisplayListener;
 import jexer.tterminal.ECMA48;
+import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
 /**
  * TTerminalWidget exposes a ECMA-48 / ANSI X3.64 style terminal in a widget.
  */
 public class TTerminalWidget extends TScrollableWidget
-                             implements DisplayListener {
+                             implements DisplayListener, EditMenuUser {
 
     /**
      * Translated strings.
@@ -84,6 +79,11 @@ public class TTerminalWidget extends TScrollableWidget
      */
     private Process shell;
 
+    /**
+     * If true, something called 'ptypipe' is on the PATH and executable.
+     */
+    private static boolean ptypipeOnPath = false;
+
     /**
      * If true, we are using the ptypipe utility to support dynamic window
      * resizing.  ptypipe is available at
@@ -163,6 +163,13 @@ public class TTerminalWidget extends TScrollableWidget
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Static constructor.
+     */
+    static {
+        checkForPtypipe();
+    }
+
     /**
      * Public constructor spawns a custom command line.
      *
@@ -198,7 +205,7 @@ public class TTerminalWidget extends TScrollableWidget
      * @param x column relative to parent
      * @param y row relative to parent
      * @param command the command line to execute
-     * @param closeAction action to perform when the shell sxits
+     * @param closeAction action to perform when the shell exits
      */
     public TTerminalWidget(final TWidget parent, final int x, final int y,
         final String [] command, final TAction closeAction) {
@@ -215,7 +222,7 @@ public class TTerminalWidget extends TScrollableWidget
      * @param width width of widget
      * @param height height of widget
      * @param command the command line to execute
-     * @param closeAction action to perform when the shell sxits
+     * @param closeAction action to perform when the shell exits
      */
     public TTerminalWidget(final TWidget parent, final int x, final int y,
         final int width, final int height, final String [] command,
@@ -236,6 +243,14 @@ public class TTerminalWidget extends TScrollableWidget
             fullCommand = new String[command.length + 1];
             fullCommand[0] = "ptypipe";
             System.arraycopy(command, 0, fullCommand, 1, command.length);
+        } else if (System.getProperty("jexer.TTerminal.ptypipe",
+                "auto").equals("auto")
+            && (ptypipeOnPath == true)
+        ) {
+            ptypipe = true;
+            fullCommand = new String[command.length + 1];
+            fullCommand[0] = "ptypipe";
+            System.arraycopy(command, 0, fullCommand, 1, command.length);
         } else if (System.getProperty("os.name").startsWith("Windows")) {
             fullCommand = new String[3];
             fullCommand[0] = "cmd";
@@ -251,12 +266,24 @@ public class TTerminalWidget extends TScrollableWidget
             fullCommand[5] = stringArrayToString(command);
         } else {
             // Default: behave like Linux
-            fullCommand = new String[5];
-            fullCommand[0] = "script";
-            fullCommand[1] = "-fqe";
-            fullCommand[2] = "/dev/null";
-            fullCommand[3] = "-c";
-            fullCommand[4] = stringArrayToString(command);
+            if (System.getProperty("jexer.TTerminal.setsid",
+                    "true").equals("false")
+            ) {
+                fullCommand = new String[5];
+                fullCommand[0] = "script";
+                fullCommand[1] = "-fqe";
+                fullCommand[2] = "/dev/null";
+                fullCommand[3] = "-c";
+                fullCommand[4] = stringArrayToString(command);
+            } else {
+                fullCommand = new String[6];
+                fullCommand[0] = "setsid";
+                fullCommand[1] = "script";
+                fullCommand[2] = "-fqe";
+                fullCommand[3] = "/dev/null";
+                fullCommand[4] = "-c";
+                fullCommand[5] = stringArrayToString(command);
+            }
         }
         spawnShell(fullCommand);
     }
@@ -278,7 +305,7 @@ public class TTerminalWidget extends TScrollableWidget
      * @param parent parent widget
      * @param x column relative to parent
      * @param y row relative to parent
-     * @param closeAction action to perform when the shell sxits
+     * @param closeAction action to perform when the shell exits
      */
     public TTerminalWidget(final TWidget parent, final int x, final int y,
         final TAction closeAction) {
@@ -294,7 +321,7 @@ public class TTerminalWidget extends TScrollableWidget
      * @param y row relative to parent
      * @param width width of widget
      * @param height height of widget
-     * @param closeAction action to perform when the shell sxits
+     * @param closeAction action to perform when the shell exits
      */
     public TTerminalWidget(final TWidget parent, final int x, final int y,
         final int width, final int height, final TAction closeAction) {
@@ -320,6 +347,7 @@ public class TTerminalWidget extends TScrollableWidget
         // GNU differ on the '-f' vs '-F' flags, we need two different
         // commands.  Lovely.
         String cmdShellGNU = "script -fqe /dev/null";
+        String cmdShellGNUSetsid = "setsid script -fqe /dev/null";
         String cmdShellBSD = "script -q -F /dev/null";
 
         // ptypipe is another solution that permits dynamic window resizing.
@@ -332,12 +360,24 @@ public class TTerminalWidget extends TScrollableWidget
         ) {
             ptypipe = true;
             spawnShell(cmdShellPtypipe.split("\\s+"));
+        } else if (System.getProperty("jexer.TTerminal.ptypipe",
+                "auto").equals("auto")
+            && (ptypipeOnPath == true)
+        ) {
+            ptypipe = true;
+            spawnShell(cmdShellPtypipe.split("\\s+"));
         } else if (System.getProperty("os.name").startsWith("Windows")) {
             spawnShell(cmdShellWindows.split("\\s+"));
         } else if (System.getProperty("os.name").startsWith("Mac")) {
             spawnShell(cmdShellBSD.split("\\s+"));
         } else if (System.getProperty("os.name").startsWith("Linux")) {
-            spawnShell(cmdShellGNU.split("\\s+"));
+            if (System.getProperty("jexer.TTerminal.setsid",
+                    "true").equals("false")
+            ) {
+                spawnShell(cmdShellGNU.split("\\s+"));
+            } else {
+                spawnShell(cmdShellGNUSetsid.split("\\s+"));
+            }
         } else {
             // When all else fails, assume GNU.
             spawnShell(cmdShellGNU.split("\\s+"));
@@ -525,6 +565,32 @@ public class TTerminalWidget extends TScrollableWidget
         super.onMouseMotion(mouse);
     }
 
+    /**
+     * Handle posted command events.
+     *
+     * @param command command event
+     */
+    @Override
+    public void onCommand(final TCommandEvent command) {
+        if (emulator == null) {
+            return;
+        }
+
+        if (command.equals(cmPaste)) {
+            // Paste text from clipboard.
+            String text = getClipboard().pasteText();
+            if (text != null) {
+                for (int i = 0; i < text.length(); ) {
+                    int ch = text.codePointAt(i);
+                    emulator.addUserEvent(new TKeypressEvent(false, 0, ch,
+                            false, false, false));
+                    i += Character.charCount(ch);
+                }
+            }
+            return;
+        }
+    }
+
     // ------------------------------------------------------------------------
     // TScrollableWidget ------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -541,9 +607,7 @@ public class TTerminalWidget extends TScrollableWidget
         int width = getDisplayWidth();
 
         boolean syncEmulator = false;
-        if ((System.currentTimeMillis() - lastUpdateTime >= 20)
-            && (dirty == true)
-        ) {
+        if (System.currentTimeMillis() - lastUpdateTime >= 50) {
             // Too much time has passed, draw it all.
             syncEmulator = true;
         } else if (emulator.isReading() && (dirty == false)) {
@@ -731,6 +795,43 @@ public class TTerminalWidget extends TScrollableWidget
     // TTerminalWidget --------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Check for 'ptypipe' on the path.  If available, set ptypipeOnPath.
+     */
+    private static void checkForPtypipe() {
+        String systemPath = System.getenv("PATH");
+        if (systemPath == null) {
+            return;
+        }
+
+        String [] paths = systemPath.split(File.pathSeparator);
+        if (paths == null) {
+            return;
+        }
+        if (paths.length == 0) {
+            return;
+        }
+        for (int i = 0; i < paths.length; i++) {
+            File path = new File(paths[i]);
+            if (path.exists() && path.isDirectory()) {
+                File [] files = path.listFiles();
+                if (files == null) {
+                    continue;
+                }
+                if (files.length == 0) {
+                    continue;
+                }
+                for (int j = 0; j < files.length; j++) {
+                    File file = files[j];
+                    if (file.canExecute() && file.getName().equals("ptypipe")) {
+                        ptypipeOnPath = true;
+                        return;
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Get the desired window title.
      *
@@ -884,10 +985,7 @@ public class TTerminalWidget extends TScrollableWidget
                     }
                 });
             }
-            if (getApplication() != null) {
-                getApplication().postEvent(new TMenuEvent(
-                    TMenu.MID_REPAINT));
-            }
+            app.doRepaint();
         }
     }
 
@@ -957,6 +1055,19 @@ public class TTerminalWidget extends TScrollableWidget
         } // synchronized (emulator)
     }
 
+    /**
+     * Wait for a period of time to get output from the launched process.
+     *
+     * @param millis millis to wait for, or 0 to wait forever
+     * @return true if the launched process has emitted something
+     */
+    public boolean waitForOutput(final int millis) {
+        if (emulator == null) {
+            return false;
+        }
+        return emulator.waitForOutput(millis);
+    }
+
     /**
      * Check if a mouse press/release/motion event coordinate is over the
      * emulator.
@@ -1125,7 +1236,17 @@ public class TTerminalWidget extends TScrollableWidget
      * Called by emulator when fresh data has come in.
      */
     public void displayChanged() {
-        dirty = true;
+        if (emulator != null) {
+            // Force sync here: EMCA48.run() thread might be setting
+            // dirty=true while TTerminalWdiget.draw() is setting
+            // dirty=false.  If these writes start interleaving, the display
+            // stops getting updated.
+            synchronized (emulator) {
+                dirty = true;
+            }
+        } else {
+            dirty = true;
+        }
         getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
     }
 
@@ -1153,4 +1274,53 @@ public class TTerminalWidget extends TScrollableWidget
         return 24;
     }
 
+    /**
+     * Get the exit value for the emulator.
+     *
+     * @return exit value
+     */
+    public int getExitValue() {
+        return exitValue;
+    }
+
+    // ------------------------------------------------------------------------
+    // EditMenuUser -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Check if the cut menu item should be enabled.
+     *
+     * @return true if the cut menu item should be enabled
+     */
+    public boolean isEditMenuCut() {
+        return false;
+    }
+
+    /**
+     * Check if the copy menu item should be enabled.
+     *
+     * @return true if the copy menu item should be enabled
+     */
+    public boolean isEditMenuCopy() {
+        return false;
+    }
+
+    /**
+     * Check if the paste menu item should be enabled.
+     *
+     * @return true if the paste menu item should be enabled
+     */
+    public boolean isEditMenuPaste() {
+        return true;
+    }
+
+    /**
+     * Check if the clear menu item should be enabled.
+     *
+     * @return true if the clear menu item should be enabled
+     */
+    public boolean isEditMenuClear() {
+        return false;
+    }
+
 }
similarity index 89%
rename from src/jexer/TTerminalWindow.java
rename to TTerminalWindow.java
index e96c50c9921d99603da342e80802c27d0da02e86..754b7a512d6f7581216a78b76c2a8a7be3838dcb 100644 (file)
  */
 package jexer;
 
-import java.awt.Font;
-import java.awt.FontMetrics;
-import java.awt.Graphics2D;
-import java.awt.image.BufferedImage;
-
-import java.io.InputStream;
-import java.io.IOException;
-import java.lang.reflect.Field;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 import java.util.ResourceBundle;
 
-import jexer.backend.ECMA48Terminal;
-import jexer.backend.GlyphMaker;
-import jexer.backend.MultiScreen;
-import jexer.backend.SwingTerminal;
-import jexer.bits.Cell;
-import jexer.bits.CellAttributes;
+import jexer.menu.TMenu;
 import jexer.event.TKeypressEvent;
 import jexer.event.TMenuEvent;
 import jexer.event.TMouseEvent;
 import jexer.event.TResizeEvent;
-import jexer.menu.TMenu;
-import jexer.tterminal.DisplayLine;
-import jexer.tterminal.DisplayListener;
-import jexer.tterminal.ECMA48;
+import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
 /**
@@ -163,10 +142,14 @@ public class TTerminalWindow extends TScrollableWindow {
         addShortcutKeys();
 
         // Add shortcut text
-        newStatusBar(i18n.getString("statusBarRunning"));
+        TStatusBar statusBar = newStatusBar(i18n.getString("statusBarRunning"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp,
+            i18n.getString("statusBarHelp"));
+        statusBar.addShortcutKeypress(kbF10, cmMenu,
+            i18n.getString("statusBarMenu"));
 
         // Spin it up
-        terminal = new TTerminalWidget(this, 0, 0, new TAction() {
+        terminal = new TTerminalWidget(this, 0, 0, command, new TAction() {
             public void DO() {
                 onShellExit();
             }
@@ -215,7 +198,11 @@ public class TTerminalWindow extends TScrollableWindow {
         addShortcutKeys();
 
         // Add shortcut text
-        newStatusBar(i18n.getString("statusBarRunning"));
+        TStatusBar statusBar = newStatusBar(i18n.getString("statusBarRunning"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp,
+            i18n.getString("statusBarHelp"));
+        statusBar.addShortcutKeypress(kbF10, cmMenu,
+            i18n.getString("statusBarMenu"));
 
         // Spin it up
         terminal = new TTerminalWidget(this, 0, 0, new TAction() {
@@ -283,7 +270,10 @@ public class TTerminalWindow extends TScrollableWindow {
      */
     @Override
     public void onKeypress(final TKeypressEvent keypress) {
-        if ((terminal != null) && (terminal.isReading())) {
+        if ((terminal != null)
+            && (terminal.isReading())
+            && (!inKeyboardResize)
+        ) {
             terminal.onKeypress(keypress);
         } else {
             super.onKeypress(keypress);
@@ -352,6 +342,16 @@ public class TTerminalWindow extends TScrollableWindow {
         }
     }
 
+    /**
+     * Get this window's help topic to load.
+     *
+     * @return the topic name
+     */
+    @Override
+    public String getHelpTopic() {
+        return "Terminal Window";
+    }
+
     // ------------------------------------------------------------------------
     // TTerminalWindow --------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -452,4 +452,29 @@ public class TTerminalWindow extends TScrollableWindow {
         getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
     }
 
+    /**
+     * Wait for a period of time to get output from the launched process.
+     *
+     * @param millis millis to wait for, or 0 to wait forever
+     * @return true if the launched process has emitted something
+     */
+    public boolean waitForOutput(final int millis) {
+        if (terminal == null) {
+            return false;
+        }
+        return terminal.waitForOutput(millis);
+    }
+
+    /**
+     * Get the exit value for the emulator.
+     *
+     * @return exit value
+     */
+    public int getExitValue() {
+        if (terminal == null) {
+            return -1;
+        }
+        return terminal.getExitValue();
+    }
+
 }
similarity index 64%
rename from src/jexer/TTerminalWindow.properties
rename to TTerminalWindow.properties
index ed22f492a49257944595dc19d53cbb2f3d3d9429..44a19f6809157d15e6098ad24eef59b8db913b80 100644 (file)
@@ -1,2 +1,4 @@
 windowTitle=Terminal
 statusBarRunning=Terminal session executing...
+statusBarHelp=Help
+statusBarMenu=Menu
similarity index 97%
rename from src/jexer/TText.java
rename to TText.java
index 22bc4b89051d31e586a60b91a274b55d3292bade..f6d7febcc0aefdcd159aa3e5af50d72f1631be0f 100644 (file)
@@ -29,7 +29,7 @@
 package jexer;
 
 import java.util.Arrays;
-import java.util.LinkedList;
+import java.util.ArrayList;
 import java.util.List;
 
 import jexer.bits.CellAttributes;
@@ -162,7 +162,7 @@ public class TText extends TScrollableWidget {
         this.text = text;
         this.colorKey = colorKey;
 
-        lines = new LinkedList<String>();
+        lines = new ArrayList<String>();
 
         vScroller = new TVScroller(this, getWidth() - 1, 0,
             Math.max(1, getHeight() - 1));
@@ -403,7 +403,7 @@ public class TText extends TScrollableWidget {
     /**
      * Set justification.
      *
-     * @param justification LEFT, CENTER, RIGHT, or FULL
+     * @param justification NONE, LEFT, CENTER, RIGHT, or FULL
      */
     public void setJustification(final Justification justification) {
         this.justification = justification;
@@ -442,4 +442,12 @@ public class TText extends TScrollableWidget {
         reflowData();
     }
 
+    /**
+     * Un-justify the text.
+     */
+    public void unJustify() {
+        justification = Justification.NONE;
+        reflowData();
+    }
+
 }
similarity index 100%
rename from src/jexer/TTimer.java
rename to TTimer.java
similarity index 100%
rename from src/jexer/TVScroller.java
rename to TVScroller.java
similarity index 98%
rename from src/jexer/TWidget.java
rename to TWidget.java
index f11cbf35c62d3aa53d19040343c47a492109d019..ce22330576309206861b8246be3423a248499180 100644 (file)
@@ -36,6 +36,7 @@ import java.util.ArrayList;
 import jexer.backend.Screen;
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
 import jexer.bits.ColorTheme;
 import jexer.event.TCommandEvent;
 import jexer.event.TInputEvent;
@@ -591,9 +592,8 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @param command command event
      */
     public void onCommand(final TCommandEvent command) {
-        // Default: do nothing, pass to children instead
-        for (TWidget widget: children) {
-            widget.onCommand(command);
+        if (activeChild != null) {
+            activeChild.onCommand(command);
         }
     }
 
@@ -1126,6 +1126,18 @@ public abstract class TWidget implements Comparable<TWidget> {
         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>
@@ -1139,7 +1151,7 @@ public abstract class TWidget implements Comparable<TWidget> {
      * difference between this.z and that.z, or String.compareTo(text)
      */
     @Override
-    public final int compareTo(final TWidget that) {
+    public int compareTo(final TWidget that) {
         if ((this instanceof TWindow)
             && (that instanceof TWindow)
         ) {
@@ -1362,7 +1374,7 @@ public abstract class TWidget implements Comparable<TWidget> {
      *
      * @param child TWidget to add
      */
-    private void addChild(final TWidget child) {
+    public void addChild(final TWidget child) {
         children.add(child);
 
         if ((child.enabled)
@@ -1403,6 +1415,19 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @return TRUE if the child was removed, FALSE if it was not found
      */
     public boolean removeChild(final TWidget child) {
+<<<<<<< HEAD:TWidget.java
+        if (children.remove(child)) {
+                child.close();
+                child.parent = null;
+                child.window = null;
+                
+                resetTabOrder();
+                
+                return true;
+        }
+        
+        return false;
+=======
        if (children.remove(child)) {
                child.close();
                child.parent = null;
@@ -1414,6 +1439,7 @@ public abstract class TWidget implements Comparable<TWidget> {
        }
        
        return false;
+>>>>>>> upstream-sep2019-tcombo:src/jexer/TWidget.java
     }
 
     /**
@@ -1439,9 +1465,9 @@ public abstract class TWidget implements Comparable<TWidget> {
                 if (activeChild != null) {
                     activeChild.active = false;
                 }
-                child.active = true;
-                activeChild = child;
             }
+            child.active = true;
+            activeChild = child;
         }
     }
 
@@ -2168,6 +2194,21 @@ public abstract class TWidget implements Comparable<TWidget> {
         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.
      *
similarity index 98%
rename from src/jexer/TWindow.java
rename to TWindow.java
index 58195c915f1ae885485891f159539f53ece7b474..4d14d0eee2debcf23b03e2df314ea41721c38c8c 100644 (file)
@@ -199,6 +199,11 @@ public class TWindow extends TWidget {
      */
     private boolean hideMouse = false;
 
+    /**
+     * The help topic for this window.
+     */
+    protected String helpTopic = "Help";
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -541,12 +546,6 @@ public class TWindow extends TWidget {
         }
 
         if (inWindowResize) {
-            // Do not permit resizing below the status line
-            if (mouse.getAbsoluteY() == application.getDesktopBottom()) {
-                inWindowResize = false;
-                return;
-            }
-
             // Move window over
             setWidth(resizeWindowWidth + (mouse.getAbsoluteX()
                     - moveWindowMouseX));
@@ -566,23 +565,22 @@ public class TWindow extends TWidget {
             // Keep within min/max bounds
             if (getWidth() < minimumWindowWidth) {
                 setWidth(minimumWindowWidth);
-                inWindowResize = false;
             }
             if (getHeight() < minimumWindowHeight) {
                 setHeight(minimumWindowHeight);
-                inWindowResize = false;
             }
             if ((maximumWindowWidth > 0)
                 && (getWidth() > maximumWindowWidth)
             ) {
                 setWidth(maximumWindowWidth);
-                inWindowResize = false;
             }
             if ((maximumWindowHeight > 0)
                 && (getHeight() > maximumWindowHeight)
             ) {
                 setHeight(maximumWindowHeight);
-                inWindowResize = false;
+            }
+            if (getHeight() + getY() >= getApplication().getDesktopBottom()) {
+                setHeight(getApplication().getDesktopBottom() - getY());
             }
 
             // Pass a resize event to my children
@@ -1440,6 +1438,15 @@ public class TWindow extends TWidget {
         this.hideMouse = hideMouse;
     }
 
+    /**
+     * Get this window's help topic to load.
+     *
+     * @return the topic name
+     */
+    public String getHelpTopic() {
+        return helpTopic;
+    }
+
     /**
      * Generate a human-readable string for this window.
      *
@@ -1447,8 +1454,9 @@ public class TWindow extends TWidget {
      */
     @Override
     public String toString() {
-        return String.format("%s(%8x) \'%s\' position (%d, %d) geometry %dx%d" +
-            " hidden %s modal %s", getClass().getName(), hashCode(), title,
+        return String.format("%s(%8x) \'%s\' Z %d position (%d, %d) " +
+            "geometry %dx%d  hidden %s modal %s",
+            getClass().getName(), hashCode(), title, getZ(),
             getX(), getY(), getWidth(), getHeight(), hidden, isModal());
     }
 
similarity index 92%
rename from src/jexer/backend/ECMA48Terminal.java
rename to backend/ECMA48Terminal.java
index e2997d2f6b17486356ddd9d902d114039258bdef..429e698d733177cd59f27095088afd71b4b4ec1f 100644 (file)
@@ -28,6 +28,9 @@
  */
 package jexer.backend;
 
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
 import java.awt.image.BufferedImage;
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
@@ -47,10 +50,10 @@ import java.util.HashMap;
 import java.util.List;
 import javax.imageio.ImageIO;
 
-import jexer.TImage;
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
 import jexer.bits.Color;
+import jexer.bits.StringUtils;
 import jexer.event.TCommandEvent;
 import jexer.event.TInputEvent;
 import jexer.event.TKeypressEvent;
@@ -83,6 +86,16 @@ public class ECMA48Terminal extends LogicalScreen
         MOUSE_SGR,
     }
 
+    /**
+     * Available Jexer images support.
+     */
+    private enum JexerImageOption {
+        DISABLED,
+        JPG,
+        PNG,
+        RGB,
+    }
+
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -189,6 +202,11 @@ public class ECMA48Terminal extends LogicalScreen
      */
     private boolean sixel = true;
 
+    /**
+     * If true, use a single shared palette for sixel.
+     */
+    private boolean sixelSharedPalette = true;
+
     /**
      * The sixel palette handler.
      */
@@ -217,20 +235,16 @@ public class ECMA48Terminal extends LogicalScreen
     private ImageCache iterm2Cache = null;
 
     /**
-     * If true, emit image data via Jexer image protocol.
+     * If not DISABLED, emit image data via Jexer image protocol if the
+     * terminal supports it.
      */
-    private boolean jexerImages = false;
+    private JexerImageOption jexerImageOption = JexerImageOption.JPG;
 
     /**
      * The Jexer post-rendered string cache.
      */
     private ImageCache jexerCache = null;
 
-    /**
-     * Base64 encoder used by iTerm2 and Jexer images.
-     */
-    private java.util.Base64.Encoder base64 = null;
-
     /**
      * If true, then we changed System.in and need to change it back.
      */
@@ -1156,11 +1170,12 @@ public class ECMA48Terminal extends LogicalScreen
 
         // Enable mouse reporting and metaSendsEscape
         this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
-        this.output.flush();
 
         // Request xterm use the sixel settings we want
         this.output.printf("%s", xtermSetSixelSettings());
 
+        this.output.flush();
+
         // Query the screen size
         sessionInfo.queryWindowSize();
         setDimensions(sessionInfo.getWindowWidth(),
@@ -1248,11 +1263,12 @@ public class ECMA48Terminal extends LogicalScreen
 
         // Enable mouse reporting and metaSendsEscape
         this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
-        this.output.flush();
 
         // Request xterm use the sixel settings we want
         this.output.printf("%s", xtermSetSixelSettings());
 
+        this.output.flush();
+
         // Query the screen size
         sessionInfo.queryWindowSize();
         setDimensions(sessionInfo.getWindowWidth(),
@@ -1479,7 +1495,15 @@ public class ECMA48Terminal extends LogicalScreen
             // SQUASH
         }
 
-        // Default to using images for full-width characters.
+        // Shared palette
+        if (System.getProperty("jexer.ECMA48.sixelSharedPalette",
+                "true").equals("false")) {
+            sixelSharedPalette = false;
+        } else {
+            sixelSharedPalette = true;
+        }
+
+        // Default to not supporting iTerm2 images.
         if (System.getProperty("jexer.ECMA48.iTerm2Images",
                 "false").equals("true")) {
             iterm2Images = true;
@@ -1487,6 +1511,19 @@ public class ECMA48Terminal extends LogicalScreen
             iterm2Images = false;
         }
 
+        // Default to using JPG Jexer images if terminal supports it.
+        String jexerImageStr = System.getProperty("jexer.ECMA48.jexerImages",
+            "jpg").toLowerCase();
+        if (jexerImageStr.equals("false")) {
+            jexerImageOption = JexerImageOption.DISABLED;
+        } else if (jexerImageStr.equals("jpg")) {
+            jexerImageOption = JexerImageOption.JPG;
+        } else if (jexerImageStr.equals("png")) {
+            jexerImageOption = JexerImageOption.PNG;
+        } else if (jexerImageStr.equals("rgb")) {
+            jexerImageOption = JexerImageOption.RGB;
+        }
+
         // Set custom colors
         setCustomSystemColors();
     }
@@ -1608,7 +1645,10 @@ public class ECMA48Terminal extends LogicalScreen
      * @return the width in pixels of a character cell
      */
     public int getTextWidth() {
-        return (widthPixels / sessionInfo.getWindowWidth());
+        if (sessionInfo.getWindowWidth() > 0) {
+            return (widthPixels / sessionInfo.getWindowWidth());
+        }
+        return 16;
     }
 
     /**
@@ -1617,7 +1657,10 @@ public class ECMA48Terminal extends LogicalScreen
      * @return the height in pixels of a character cell
      */
     public int getTextHeight() {
-        return (heightPixels / sessionInfo.getWindowHeight());
+        if (sessionInfo.getWindowHeight() > 0) {
+            return (heightPixels / sessionInfo.getWindowHeight());
+        }
+        return 20;
     }
 
     /**
@@ -2052,7 +2095,7 @@ public class ECMA48Terminal extends LogicalScreen
                 if (cellsToDraw.size() > 0) {
                     if (iterm2Images) {
                         sb.append(toIterm2Image(x, y, cellsToDraw));
-                    } else if (jexerImages) {
+                    } else if (jexerImageOption != JexerImageOption.DISABLED) {
                         sb.append(toJexerImage(x, y, cellsToDraw));
                     } else {
                         sb.append(toSixel(x, y, cellsToDraw));
@@ -2203,10 +2246,13 @@ public class ECMA48Terminal extends LogicalScreen
         boolean eventMouse3 = false;
         boolean eventMouseWheelUp = false;
         boolean eventMouseWheelDown = false;
+        boolean eventAlt = false;
+        boolean eventCtrl = false;
+        boolean eventShift = false;
 
         // System.err.printf("buttons: %04x\r\n", buttons);
 
-        switch (buttons) {
+        switch (buttons & 0xE3) {
         case 0:
             eventMouse1 = true;
             mouse1 = true;
@@ -2288,9 +2334,21 @@ public class ECMA48Terminal extends LogicalScreen
             eventType = TMouseEvent.Type.MOUSE_MOTION;
             break;
         }
+
+        if ((buttons & 0x04) != 0) {
+            eventShift = true;
+        }
+        if ((buttons & 0x08) != 0) {
+            eventAlt = true;
+        }
+        if ((buttons & 0x10) != 0) {
+            eventCtrl = true;
+        }
+
         return new TMouseEvent(eventType, x, y, x, y,
             eventMouse1, eventMouse2, eventMouse3,
-            eventMouseWheelUp, eventMouseWheelDown);
+            eventMouseWheelUp, eventMouseWheelDown,
+            eventAlt, eventCtrl, eventShift);
     }
 
     /**
@@ -2325,12 +2383,15 @@ public class ECMA48Terminal extends LogicalScreen
         boolean eventMouse3 = false;
         boolean eventMouseWheelUp = false;
         boolean eventMouseWheelDown = false;
+        boolean eventAlt = false;
+        boolean eventCtrl = false;
+        boolean eventShift = false;
 
         if (release) {
             eventType = TMouseEvent.Type.MOUSE_UP;
         }
 
-        switch (buttons) {
+        switch (buttons & 0xE3) {
         case 0:
             eventMouse1 = true;
             break;
@@ -2387,9 +2448,21 @@ public class ECMA48Terminal extends LogicalScreen
             // Unknown, bail out
             return null;
         }
+
+        if ((buttons & 0x04) != 0) {
+            eventShift = true;
+        }
+        if ((buttons & 0x08) != 0) {
+            eventAlt = true;
+        }
+        if ((buttons & 0x10) != 0) {
+            eventCtrl = true;
+        }
+
         return new TMouseEvent(eventType, x, y, x, y,
             eventMouse1, eventMouse2, eventMouse3,
-            eventMouseWheelUp, eventMouseWheelDown);
+            eventMouseWheelUp, eventMouseWheelDown,
+            eventAlt, eventCtrl, eventShift);
     }
 
     /**
@@ -2806,6 +2879,8 @@ public class ECMA48Terminal extends LogicalScreen
                     if (decPrivateModeFlag == false) {
                         break;
                     }
+                    boolean reportsJexerImages = false;
+                    boolean reportsIterm2Images = false;
                     for (String x: params) {
                         if (x.equals("4")) {
                             // Terminal reports sixel support
@@ -2818,9 +2893,27 @@ public class ECMA48Terminal extends LogicalScreen
                             if (debugToStderr) {
                                 System.err.println("Device Attributes: Jexer images");
                             }
-                            jexerImages = true;
+                            reportsJexerImages = true;
+                        }
+                        if (x.equals("1337")) {
+                            // Terminal reports iTerm2 images support
+                            if (debugToStderr) {
+                                System.err.println("Device Attributes: iTerm2 images");
+                            }
+                            reportsIterm2Images = true;
                         }
                     }
+                    if (reportsJexerImages == false) {
+                        // Terminal does not support Jexer images, disable
+                        // them.
+                        jexerImageOption = JexerImageOption.DISABLED;
+                    }
+                    if (reportsIterm2Images == false) {
+                        // Terminal does not support iTerm2 images, disable
+                        // them.
+                        iterm2Images = false;
+                    }
+                    resetParser();
                     return;
                 case 't':
                     // windowOps
@@ -2900,12 +2993,16 @@ public class ECMA48Terminal extends LogicalScreen
      *   - enable sixel scrolling
      *
      *   - disable private color registers (so that we can use one common
-     *     palette)
+     *     palette) if sixelSharedPalette is set
      *
      * @return the string to emit to xterm
      */
     private String xtermSetSixelSettings() {
-        return "\033[?80h\033[?1070l";
+        if (sixelSharedPalette == true) {
+            return "\033[?80h\033[?1070l";
+        } else {
+            return "\033[?80h\033[?1070h";
+        }
     }
 
     /**
@@ -3022,8 +3119,9 @@ public class ECMA48Terminal extends LogicalScreen
 
         if (palette == null) {
             palette = new SixelPalette();
-            // TODO: make this an option (shared palette or not)
-            palette.emitPalette(sb, null);
+            if (sixelSharedPalette == true) {
+                palette.emitPalette(sb, null);
+            }
         }
 
         return sb.toString();
@@ -3071,9 +3169,8 @@ public class ECMA48Terminal extends LogicalScreen
         if (y == height - 1) {
             // We are on the bottom row.  If scrolling mode is enabled
             // (default), then VT320/xterm will scroll the entire screen if
-            // we draw any pixels here.
-
-            // TODO: support sixel scrolling mode disabled as an option.
+            // we draw any pixels here.  Do not draw the image, bail out
+            // instead.
             sb.append(normal());
             sb.append(gotoXY(x, y));
             for (int j = 0; j < cells.size(); j++) {
@@ -3106,112 +3203,15 @@ public class ECMA48Terminal extends LogicalScreen
             // System.err.println("CACHE MISS");
         }
 
-        int imageWidth = cells.get(0).getImage().getWidth();
-        int imageHeight = cells.get(0).getImage().getHeight();
-
-        // cells.get(x).getImage() has a dithered bitmap containing indexes
-        // into the color palette.  Piece these together into one larger
-        // image for final rendering.
-        int totalWidth = 0;
-        int fullWidth = cells.size() * getTextWidth();
-        int fullHeight = getTextHeight();
-        for (int i = 0; i < cells.size(); i++) {
-            totalWidth += cells.get(i).getImage().getWidth();
-        }
-
-        BufferedImage image = new BufferedImage(fullWidth,
-            fullHeight, BufferedImage.TYPE_INT_ARGB);
-
-        int [] rgbArray;
-        for (int i = 0; i < cells.size() - 1; i++) {
-            int tileWidth = Math.min(cells.get(i).getImage().getWidth(),
-                imageWidth);
-            int tileHeight = Math.min(cells.get(i).getImage().getHeight(),
-                imageHeight);
-
-            if (false && cells.get(i).isInvertedImage()) {
-                // I used to put an all-white cell over the cursor, don't do
-                // that anymore.
-                rgbArray = new int[imageWidth * imageHeight];
-                for (int j = 0; j < rgbArray.length; j++) {
-                    rgbArray[j] = 0xFFFFFF;
-                }
-            } else {
-                try {
-                    rgbArray = cells.get(i).getImage().getRGB(0, 0,
-                        tileWidth, tileHeight, null, 0, tileWidth);
-                } catch (Exception e) {
-                    throw new RuntimeException("image " + imageWidth + "x" +
-                        imageHeight +
-                        "tile " + tileWidth + "x" +
-                        tileHeight +
-                        " cells.get(i).getImage() " +
-                        cells.get(i).getImage() +
-                        " i " + i +
-                        " fullWidth " + fullWidth +
-                        " fullHeight " + fullHeight, e);
-                }
-            }
-
-            /*
-            System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
-                i * imageWidth, 0, imageWidth, imageHeight,
-                0, imageWidth);
-            System.err.printf("   fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
-                fullWidth, fullHeight, cells.size(), getTextWidth());
-             */
-
-            image.setRGB(i * imageWidth, 0, tileWidth, tileHeight,
-                rgbArray, 0, tileWidth);
-            if (tileHeight < fullHeight) {
-                int backgroundColor = cells.get(i).getBackground().getRGB();
-                for (int imageX = 0; imageX < image.getWidth(); imageX++) {
-                    for (int imageY = imageHeight; imageY < fullHeight;
-                         imageY++) {
-
-                        image.setRGB(imageX, imageY, backgroundColor);
-                    }
-                }
-            }
-        }
-        totalWidth -= ((cells.size() - 1) * imageWidth);
-        if (false && cells.get(cells.size() - 1).isInvertedImage()) {
-            // I used to put an all-white cell over the cursor, don't do that
-            // anymore.
-            rgbArray = new int[totalWidth * imageHeight];
-            for (int j = 0; j < rgbArray.length; j++) {
-                rgbArray[j] = 0xFFFFFF;
-            }
-        } else {
-            try {
-                rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0,
-                    totalWidth, imageHeight, null, 0, totalWidth);
-            } catch (Exception e) {
-                throw new RuntimeException("image " + imageWidth + "x" +
-                    imageHeight + " cells.get(cells.size() - 1).getImage() " +
-                    cells.get(cells.size() - 1).getImage(), e);
-            }
-        }
-        image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
-            imageHeight, rgbArray, 0, totalWidth);
-
-        if (totalWidth < getTextWidth()) {
-            int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
-
-            for (int imageX = image.getWidth() - totalWidth;
-                 imageX < image.getWidth(); imageX++) {
-
-                for (int imageY = 0; imageY < fullHeight; imageY++) {
-                    image.setRGB(imageX, imageY, backgroundColor);
-                }
-            }
-        }
+        BufferedImage image = cellsToImage(cells);
+        int fullHeight = image.getHeight();
 
         // Dither the image.  It is ok to lose the original here.
         if (palette == null) {
             palette = new SixelPalette();
-            // TODO: make this an option (shared palette or not)
-            palette.emitPalette(sb, null);
+            if (sixelSharedPalette == true) {
+                palette.emitPalette(sb, null);
+            }
         }
         image = palette.ditherImage(image);
 
@@ -3219,20 +3219,17 @@ public class ECMA48Terminal extends LogicalScreen
         int rasterHeight = 0;
         int rasterWidth = image.getWidth();
 
-        /*
-
-        // TODO: make this an option (shared palette or not)
-
-        // Emit the palette, but only for the colors actually used by these
-        // cells.
-        boolean [] usedColors = new boolean[sixelPaletteSize];
-        for (int imageX = 0; imageX < image.getWidth(); imageX++) {
-            for (int imageY = 0; imageY < image.getHeight(); imageY++) {
-                usedColors[image.getRGB(imageX, imageY)] = true;
+        if (sixelSharedPalette == false) {
+            // Emit the palette, but only for the colors actually used by
+            // these cells.
+            boolean [] usedColors = new boolean[sixelPaletteSize];
+            for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+                for (int imageY = 0; imageY < image.getHeight(); imageY++) {
+                    usedColors[image.getRGB(imageX, imageY)] = true;
+                }
             }
+            palette.emitPalette(sb, usedColors);
         }
-        palette.emitPalette(sb, usedColors);
-         */
 
         // Render the entire row of cells.
         for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) {
@@ -3362,74 +3359,23 @@ public class ECMA48Terminal extends LogicalScreen
         return sixel;
     }
 
-    // ------------------------------------------------------------------------
-    // End sixel output support -----------------------------------------------
-    // ------------------------------------------------------------------------
-
-    // ------------------------------------------------------------------------
-    // iTerm2 image output support --------------------------------------------
-    // ------------------------------------------------------------------------
-
     /**
-     * Create an iTerm2 images string representing a row of several cells
-     * containing bitmap data.
+     * Convert a horizontal range of cell's image data into a single
+     * contigous image, rescaled and anti-aliased to match the current text
+     * cell size.
      *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param cells the cells containing the bitmap data
-     * @return the string to emit to an ANSI / ECMA-style terminal
+     * @param cells the cells containing image data
+     * @return the image resized to the current text cell size
      */
-    private String toIterm2Image(final int x, final int y,
-        final ArrayList<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();
         }
@@ -3439,10 +3385,9 @@ public class ECMA48Terminal extends LogicalScreen
 
         int [] rgbArray;
         for (int i = 0; i < cells.size() - 1; i++) {
-            int tileWidth = Math.min(cells.get(i).getImage().getWidth(),
-                imageWidth);
-            int tileHeight = Math.min(cells.get(i).getImage().getHeight(),
-                imageHeight);
+            int tileWidth = imageWidth;
+            int tileHeight = imageHeight;
+
             if (false && cells.get(i).isInvertedImage()) {
                 // I used to put an all-white cell over the cursor, don't do
                 // that anymore.
@@ -3509,7 +3454,7 @@ public class ECMA48Terminal extends LogicalScreen
         image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
             imageHeight, rgbArray, 0, totalWidth);
 
-        if (totalWidth < getTextWidth()) {
+        if (totalWidth < imageWidth) {
             int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
 
             for (int imageX = image.getWidth() - totalWidth;
@@ -3521,6 +3466,91 @@ public class ECMA48Terminal extends LogicalScreen
             }
         }
 
+        if ((image.getWidth() != cells.size() * getTextWidth())
+            || (image.getHeight() != getTextHeight())
+        ) {
+            // Rescale the image to fit the text cells it is going into.
+            BufferedImage newImage;
+            newImage = new BufferedImage(cells.size() * getTextWidth(),
+                getTextHeight(), BufferedImage.TYPE_INT_ARGB);
+
+            Graphics gr = newImage.getGraphics();
+            if (gr instanceof Graphics2D) {
+                ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                    RenderingHints.VALUE_ANTIALIAS_ON);
+                ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING,
+                    RenderingHints.VALUE_RENDER_QUALITY);
+            }
+            gr.drawImage(image, 0, 0, newImage.getWidth(),
+                newImage.getHeight(), null, null);
+            gr.dispose();
+            image = newImage;
+        }
+
+        return image;
+    }
+
+    // ------------------------------------------------------------------------
+    // End sixel output support -----------------------------------------------
+    // ------------------------------------------------------------------------
+
+    // ------------------------------------------------------------------------
+    // iTerm2 image output support --------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Create an iTerm2 images string representing a row of several cells
+     * containing bitmap data.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param cells the cells containing the bitmap data
+     * @return the string to emit to an ANSI / ECMA-style terminal
+     */
+    private String toIterm2Image(final int x, final int y,
+        final ArrayList<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:
          *
@@ -3582,8 +3612,6 @@ public class ECMA48Terminal extends LogicalScreen
             return "";
         }
 
-        // iTerm2 does not advance the cursor automatically, so place it
-        // myself.
         sb.append("\033]1337;File=");
         /*
         sb.append(String.format("width=$d;height=1;preserveAspectRatio=1;",
@@ -3595,7 +3623,7 @@ public class ECMA48Terminal extends LogicalScreen
                     getTextHeight())));
          */
         sb.append("inline=1:");
-        sb.append(base64.encodeToString(pngOutputStream.toByteArray()));
+        sb.append(StringUtils.toBase64(pngOutputStream.toByteArray()));
         sb.append("\007");
 
         if (saveInCache) {
@@ -3641,7 +3669,7 @@ public class ECMA48Terminal extends LogicalScreen
         assert (cells.size() > 0);
         assert (cells.get(0).getImage() != null);
 
-        if (jexerImages == false) {
+        if (jexerImageOption == JexerImageOption.DISABLED) {
             sb.append(normal());
             sb.append(gotoXY(x, y));
             for (int i = 0; i < cells.size(); i++) {
@@ -3652,7 +3680,6 @@ public class ECMA48Terminal extends LogicalScreen
 
         if (jexerCache == null) {
             jexerCache = new ImageCache(height * 10);
-            base64 = java.util.Base64.getEncoder();
         }
 
         // Save and get rows to/from the cache that do NOT have inverted
@@ -3674,121 +3701,80 @@ public class ECMA48Terminal extends LogicalScreen
             // System.err.println("CACHE MISS");
         }
 
-        int imageWidth = cells.get(0).getImage().getWidth();
-        int imageHeight = cells.get(0).getImage().getHeight();
+        BufferedImage image = cellsToImage(cells);
+        int fullHeight = image.getHeight();
 
-        // cells.get(x).getImage() has a dithered bitmap containing indexes
-        // into the color palette.  Piece these together into one larger
-        // image for final rendering.
-        int totalWidth = 0;
-        int fullWidth = cells.size() * getTextWidth();
-        int fullHeight = getTextHeight();
-        for (int i = 0; i < cells.size(); i++) {
-            totalWidth += cells.get(i).getImage().getWidth();
-        }
-
-        BufferedImage image = new BufferedImage(fullWidth,
-            fullHeight, BufferedImage.TYPE_INT_ARGB);
-
-        int [] rgbArray;
-        for (int i = 0; i < cells.size() - 1; i++) {
-            int tileWidth = Math.min(cells.get(i).getImage().getWidth(),
-                imageWidth);
-            int tileHeight = Math.min(cells.get(i).getImage().getHeight(),
-                imageHeight);
-            if (false && cells.get(i).isInvertedImage()) {
-                // I used to put an all-white cell over the cursor, don't do
-                // that anymore.
-                rgbArray = new int[imageWidth * imageHeight];
-                for (int j = 0; j < rgbArray.length; j++) {
-                    rgbArray[j] = 0xFFFFFF;
-                }
-            } else {
-                try {
-                    rgbArray = cells.get(i).getImage().getRGB(0, 0,
-                        tileWidth, tileHeight, null, 0, tileWidth);
-                } catch (Exception e) {
-                    throw new RuntimeException("image " + imageWidth + "x" +
-                        imageHeight +
-                        "tile " + tileWidth + "x" +
-                        tileHeight +
-                        " cells.get(i).getImage() " +
-                        cells.get(i).getImage() +
-                        " i " + i +
-                        " fullWidth " + fullWidth +
-                        " fullHeight " + fullHeight, e);
+        if (jexerImageOption == JexerImageOption.PNG) {
+            // Encode as PNG
+            ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream(1024);
+            try {
+                if (!ImageIO.write(image.getSubimage(0, 0, image.getWidth(),
+                            Math.min(image.getHeight(), fullHeight)),
+                        "PNG", pngOutputStream)
+                ) {
+                    // We failed to render image, bail out.
+                    return "";
                 }
+            } catch (IOException e) {
+                // We failed to render image, bail out.
+                return "";
             }
 
-            /*
-            System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
-                i * imageWidth, 0, imageWidth, imageHeight,
-                0, imageWidth);
-            System.err.printf("   fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
-                fullWidth, fullHeight, cells.size(), getTextWidth());
-             */
+            sb.append("\033]444;1;0;");
+            sb.append(StringUtils.toBase64(pngOutputStream.toByteArray()));
+            sb.append("\007");
 
-            image.setRGB(i * imageWidth, 0, tileWidth, tileHeight,
-                rgbArray, 0, tileWidth);
-            if (tileHeight < fullHeight) {
-                int backgroundColor = cells.get(i).getBackground().getRGB();
-                for (int imageX = 0; imageX < image.getWidth(); imageX++) {
-                    for (int imageY = imageHeight; imageY < fullHeight;
-                         imageY++) {
+        } else if (jexerImageOption == JexerImageOption.JPG) {
+
+            // Encode as JPG
+            ByteArrayOutputStream jpgOutputStream = new ByteArrayOutputStream(1024);
+
+            // Convert from ARGB to RGB, otherwise the JPG encode will fail.
+            BufferedImage jpgImage = new BufferedImage(image.getWidth(),
+                image.getHeight(), BufferedImage.TYPE_INT_RGB);
+            int [] pixels = new int[image.getWidth() * image.getHeight()];
+            image.getRGB(0, 0, image.getWidth(), image.getHeight(), pixels,
+                0, image.getWidth());
+            jpgImage.setRGB(0, 0, image.getWidth(), image.getHeight(), pixels,
+                0, image.getWidth());
 
-                        image.setRGB(imageX, imageY, backgroundColor);
-                    }
-                }
-            }
-        }
-        totalWidth -= ((cells.size() - 1) * imageWidth);
-        if (false && cells.get(cells.size() - 1).isInvertedImage()) {
-            // I used to put an all-white cell over the cursor, don't do that
-            // anymore.
-            rgbArray = new int[totalWidth * imageHeight];
-            for (int j = 0; j < rgbArray.length; j++) {
-                rgbArray[j] = 0xFFFFFF;
-            }
-        } else {
             try {
-                rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0,
-                    totalWidth, imageHeight, null, 0, totalWidth);
-            } catch (Exception e) {
-                throw new RuntimeException("image " + imageWidth + "x" +
-                    imageHeight + " cells.get(cells.size() - 1).getImage() " +
-                    cells.get(cells.size() - 1).getImage(), e);
+                if (!ImageIO.write(jpgImage.getSubimage(0, 0,
+                            jpgImage.getWidth(),
+                            Math.min(jpgImage.getHeight(), fullHeight)),
+                        "JPG", jpgOutputStream)
+                ) {
+                    // We failed to render image, bail out.
+                    return "";
+                }
+            } catch (IOException e) {
+                // We failed to render image, bail out.
+                return "";
             }
-        }
-        image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
-            imageHeight, rgbArray, 0, totalWidth);
 
-        if (totalWidth < getTextWidth()) {
-            int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
+            sb.append("\033]444;2;0;");
+            sb.append(StringUtils.toBase64(jpgOutputStream.toByteArray()));
+            sb.append("\007");
 
-            for (int imageX = image.getWidth() - totalWidth;
-                 imageX < image.getWidth(); imageX++) {
+        } else if (jexerImageOption == JexerImageOption.RGB) {
 
-                for (int imageY = 0; imageY < fullHeight; imageY++) {
-                    image.setRGB(imageX, imageY, backgroundColor);
-                }
-            }
-        }
+            // RGB
+            sb.append(String.format("\033]444;0;%d;%d;0;", image.getWidth(),
+                    Math.min(image.getHeight(), fullHeight)));
 
-        sb.append(String.format("\033]444;%d;%d;0;", image.getWidth(),
-                Math.min(image.getHeight(), fullHeight)));
-
-        byte [] bytes = new byte[image.getWidth() * image.getHeight() * 3];
-        int stride = image.getWidth();
-        for (int px = 0; px < stride; px++) {
-            for (int py = 0; py < image.getHeight(); py++) {
-                int rgb = image.getRGB(px, py);
-                bytes[(py * stride * 3) + (px * 3)]     = (byte) ((rgb >>> 16) & 0xFF);
-                bytes[(py * stride * 3) + (px * 3) + 1] = (byte) ((rgb >>>  8) & 0xFF);
-                bytes[(py * stride * 3) + (px * 3) + 2] = (byte) ( rgb         & 0xFF);
+            byte [] bytes = new byte[image.getWidth() * image.getHeight() * 3];
+            int stride = image.getWidth();
+            for (int px = 0; px < stride; px++) {
+                for (int py = 0; py < image.getHeight(); py++) {
+                    int rgb = image.getRGB(px, py);
+                    bytes[(py * stride * 3) + (px * 3)]     = (byte) ((rgb >>> 16) & 0xFF);
+                    bytes[(py * stride * 3) + (px * 3) + 1] = (byte) ((rgb >>>  8) & 0xFF);
+                    bytes[(py * stride * 3) + (px * 3) + 2] = (byte) ( rgb         & 0xFF);
+                }
             }
+            sb.append(StringUtils.toBase64(bytes));
+            sb.append("\007");
         }
-        sb.append(base64.encodeToString(bytes));
-        sb.append("\007");
 
         if (saveInCache) {
             // This row is OK to save into the cache.
@@ -3804,7 +3790,7 @@ public class ECMA48Terminal extends LogicalScreen
      * @return true if this terminal is emitting Jexer images
      */
     public boolean hasJexerImages() {
-        return jexerImages;
+        return (jexerImageOption != JexerImageOption.DISABLED);
     }
 
     // ------------------------------------------------------------------------
similarity index 98%
rename from src/jexer/backend/GlyphMaker.java
rename to backend/GlyphMaker.java
index 0da2918def6c8d9d0f57d7a77506828039994d69..e5fcc522da927fb2c8788b98f8a5075815389f38 100644 (file)
@@ -29,6 +29,7 @@
 package jexer.backend;
 
 import java.awt.Font;
+import java.awt.FontFormatException;
 import java.awt.FontMetrics;
 import java.awt.Graphics2D;
 import java.awt.geom.Rectangle2D;
@@ -139,7 +140,7 @@ class GlyphMakerFont {
 
         if (filename.length() == 0) {
             // Fallback font
-            font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+            font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2);
             return;
         }
 
@@ -148,16 +149,16 @@ class GlyphMakerFont {
             ClassLoader loader = Thread.currentThread().getContextClassLoader();
             InputStream in = loader.getResourceAsStream(filename);
             fontRoot = Font.createFont(Font.TRUETYPE_FONT, in);
-            font = fontRoot.deriveFont(Font.PLAIN, fontSize);
-        } catch (java.awt.FontFormatException e) {
+            font = fontRoot.deriveFont(Font.PLAIN, fontSize - 2);
+        } catch (FontFormatException e) {
             // Ideally we would report an error here, either via System.err
             // or TExceptionDialog.  However, I do not want GlyphMaker to
             // know about available backends, so we quietly fallback to
             // whatever is available as MONO.
-            font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
-        } catch (java.io.IOException e) {
+            font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2);
+        } catch (IOException e) {
             // See comment above.
-            font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+            font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2);
         }
     }
 
diff --git a/backend/HeadlessBackend.java b/backend/HeadlessBackend.java
new file mode 100644 (file)
index 0000000..19fb589
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.backend;
+
+import java.util.List;
+
+import jexer.event.TInputEvent;
+
+/**
+ * HeadlessBackend
+ */
+public class HeadlessBackend extends LogicalScreen implements Backend {
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The session information.
+     */
+    private SessionInfo sessionInfo;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     */
+    public HeadlessBackend() {
+        sessionInfo = new TSessionInfo(width, height);
+    }
+
+    // ------------------------------------------------------------------------
+    // Backend ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Getter for sessionInfo.
+     *
+     * @return the SessionInfo
+     */
+    public final SessionInfo getSessionInfo() {
+        return sessionInfo;
+    }
+
+    /**
+     * Get a Screen, which displays the text cells to the user.
+     *
+     * @return the Screen
+     */
+    public Screen getScreen() {
+        return this;
+    }
+
+    /**
+     * Subclasses must provide an implementation that syncs the logical
+     * screen to the physical device.
+     */
+    public void flushScreen() {
+        // NOP
+    }
+
+    /**
+     * Check if there are events in the queue.
+     *
+     * @return if true, getEvents() has something to return to the application
+     */
+    public boolean hasEvents() {
+        return false;
+    }
+
+    /**
+     * Subclasses must provide an implementation to get keyboard, mouse, and
+     * screen resize events.
+     *
+     * @param queue list to append new events to
+     */
+    public void getEvents(List<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
+    }
+
+}
similarity index 83%
rename from src/jexer/backend/LogicalScreen.java
rename to backend/LogicalScreen.java
index 4e4aecca7a349eb999b83a9ef4bc71c229ff650a..22b7e95f6564aad97431ca2e60a2fec09e9d2365 100644 (file)
@@ -33,6 +33,7 @@ import java.awt.image.BufferedImage;
 import jexer.backend.GlyphMaker;
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
 import jexer.bits.GraphicsChars;
 import jexer.bits.StringUtils;
 
@@ -1042,4 +1043,184 @@ public class LogicalScreen implements Screen {
         putFullwidthCharXY(x, y, cell);
     }
 
+    /**
+     * Invert the cell color at a position, including both halves of a
+     * double-width cell.
+     *
+     * @param x column position
+     * @param y row position
+     */
+    public void invertCell(final int x, final int y) {
+        invertCell(x, y, false);
+    }
+
+    /**
+     * Invert the cell color at a position.
+     *
+     * @param x column position
+     * @param y row position
+     * @param onlyThisCell if true, only invert this cell, otherwise invert
+     * both halves of a double-width cell if necessary
+     */
+    public void invertCell(final int x, final int y,
+        final boolean onlyThisCell) {
+
+        Cell cell = getCharXY(x, y);
+        if (cell.isImage()) {
+            cell.invertImage();
+        }
+        if (cell.getForeColorRGB() < 0) {
+            cell.setForeColor(cell.getForeColor().invert());
+        } else {
+            cell.setForeColorRGB(cell.getForeColorRGB() ^ 0x00ffffff);
+        }
+        if (cell.getBackColorRGB() < 0) {
+            cell.setBackColor(cell.getBackColor().invert());
+        } else {
+            cell.setBackColorRGB(cell.getBackColorRGB() ^ 0x00ffffff);
+        }
+        putCharXY(x, y, cell);
+        if ((onlyThisCell == true) || (cell.getWidth() == Cell.Width.SINGLE)) {
+            return;
+        }
+
+        // This cell is one half of a fullwidth glyph.  Invert the other
+        // half.
+        if (cell.getWidth() == Cell.Width.LEFT) {
+            if (x < width - 1) {
+                Cell rightHalf = getCharXY(x + 1, y);
+                if (rightHalf.getWidth() == Cell.Width.RIGHT) {
+                    invertCell(x + 1, y, true);
+                    return;
+                }
+            }
+        }
+        if (cell.getWidth() == Cell.Width.RIGHT) {
+            if (x > 0) {
+                Cell leftHalf = getCharXY(x - 1, y);
+                if (leftHalf.getWidth() == Cell.Width.LEFT) {
+                    invertCell(x - 1, y, true);
+                }
+            }
+        }
+    }
+
+    /**
+     * Set a selection area on the screen.
+     *
+     * @param x0 the starting X position of the selection
+     * @param y0 the starting Y position of the selection
+     * @param x1 the ending X position of the selection
+     * @param y1 the ending Y position of the selection
+     * @param rectangle if true, this is a rectangle select
+     */
+    public void setSelection(final int x0, final int y0,
+        final int x1, final int y1, final boolean rectangle) {
+
+        int startX = x0;
+        int startY = y0;
+        int endX = x1;
+        int endY = y1;
+
+        if (((x1 < x0) && (y1 == y0))
+            || (y1 < y0)
+        ) {
+            // The user dragged from bottom-to-top and/or right-to-left.
+            // Reverse the coordinates for the inverted section.
+            startX = x1;
+            startY = y1;
+            endX = x0;
+            endY = y0;
+        }
+        if (rectangle) {
+            for (int y = startY; y <= endY; y++) {
+                for (int x = startX; x <= endX; x++) {
+                    invertCell(x, y);
+                }
+            }
+        } else {
+            if (endY > startY) {
+                for (int x = startX; x < width; x++) {
+                    invertCell(x, startY);
+                }
+                for (int y = startY + 1; y < endY; y++) {
+                    for (int x = 0; x < width; x++) {
+                        invertCell(x, y);
+                    }
+                }
+                for (int x = 0; x <= endX; x++) {
+                    invertCell(x, endY);
+                }
+            } else {
+                assert (startY == endY);
+                for (int x = startX; x <= endX; x++) {
+                    invertCell(x, startY);
+                }
+            }
+        }
+    }
+
+    /**
+     * Copy the screen selection area to the clipboard.
+     *
+     * @param clipboard the clipboard to use
+     * @param x0 the starting X position of the selection
+     * @param y0 the starting Y position of the selection
+     * @param x1 the ending X position of the selection
+     * @param y1 the ending Y position of the selection
+     * @param rectangle if true, this is a rectangle select
+     */
+    public void copySelection(final Clipboard clipboard,
+        final int x0, final int y0, final int x1, final int y1,
+        final boolean rectangle) {
+
+        StringBuilder sb = new StringBuilder();
+
+        int startX = x0;
+        int startY = y0;
+        int endX = x1;
+        int endY = y1;
+
+        if (((x1 < x0) && (y1 == y0))
+            || (y1 < y0)
+        ) {
+            // The user dragged from bottom-to-top and/or right-to-left.
+            // Reverse the coordinates for the inverted section.
+            startX = x1;
+            startY = y1;
+            endX = x0;
+            endY = y0;
+        }
+        if (rectangle) {
+            for (int y = startY; y <= endY; y++) {
+                for (int x = startX; x <= endX; x++) {
+                    sb.append(Character.toChars(getCharXY(x, y).getChar()));
+                }
+                sb.append("\n");
+            }
+        } else {
+            if (endY > startY) {
+                for (int x = startX; x < width; x++) {
+                    sb.append(Character.toChars(getCharXY(x, startY).getChar()));
+                }
+                sb.append("\n");
+                for (int y = startY + 1; y < endY; y++) {
+                    for (int x = 0; x < width; x++) {
+                        sb.append(Character.toChars(getCharXY(x, y).getChar()));
+                    }
+                    sb.append("\n");
+                }
+                for (int x = 0; x <= endX; x++) {
+                    sb.append(Character.toChars(getCharXY(x, endY).getChar()));
+                }
+            } else {
+                assert (startY == endY);
+                for (int x = startX; x <= endX; x++) {
+                    sb.append(Character.toChars(getCharXY(x, startY).getChar()));
+                }
+            }
+        }
+        clipboard.copyText(sb.toString());
+    }
+
 }
similarity index 84%
rename from src/jexer/backend/MultiScreen.java
rename to backend/MultiScreen.java
index 9d66b69342896a50c6d8683d6cc4c463db4491c2..45741c05f15853eb336738267508cd1fd62e32ee 100644 (file)
@@ -33,6 +33,7 @@ import java.util.List;
 
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
 
 /**
  * MultiScreen mirrors its I/O to several screens.
@@ -93,7 +94,10 @@ public class MultiScreen implements Screen {
      * @return drawing boundary
      */
     public int getClipRight() {
-        return screens.get(0).getClipRight();
+        if (screens.size() > 0) {
+            return screens.get(0).getClipRight();
+        }
+        return 0;
     }
 
     /**
@@ -113,7 +117,10 @@ public class MultiScreen implements Screen {
      * @return drawing boundary
      */
     public int getClipBottom() {
-        return screens.get(0).getClipBottom();
+        if (screens.size() > 0) {
+            return screens.get(0).getClipBottom();
+        }
+        return 0;
     }
 
     /**
@@ -133,7 +140,10 @@ public class MultiScreen implements Screen {
      * @return drawing boundary
      */
     public int getClipLeft() {
-        return screens.get(0).getClipLeft();
+        if (screens.size() > 0) {
+            return screens.get(0).getClipLeft();
+        }
+        return 0;
     }
 
     /**
@@ -153,7 +163,10 @@ public class MultiScreen implements Screen {
      * @return drawing boundary
      */
     public int getClipTop() {
-        return screens.get(0).getClipTop();
+        if (screens.size() > 0) {
+            return screens.get(0).getClipTop();
+        }
+        return 0;
     }
 
     /**
@@ -190,7 +203,10 @@ public class MultiScreen implements Screen {
      * @return attributes at (x, y)
      */
     public CellAttributes getAttrXY(final int x, final int y) {
-        return screens.get(0).getAttrXY(x, y);
+        if (screens.size() > 0) {
+            return screens.get(0).getAttrXY(x, y);
+        }
+        return new CellAttributes();
     }
 
     /**
@@ -201,7 +217,10 @@ public class MultiScreen implements Screen {
      * @return the character + attributes
      */
     public Cell getCharXY(final int x, final int y) {
-        return screens.get(0).getCharXY(x, y);
+        if (screens.size() > 0) {
+            return screens.get(0).getCharXY(x, y);
+        }
+        return new Cell();
     }
 
     /**
@@ -410,7 +429,10 @@ public class MultiScreen implements Screen {
      */
     public int getHeight() {
         // Return the smallest height of the screens.
-        int height = screens.get(0).getHeight();
+        int height = 25;
+        if (screens.size() > 0) {
+            height = screens.get(0).getHeight();
+        }
         for (Screen screen: screens) {
             if (screen.getHeight() < height) {
                 height = screen.getHeight();
@@ -426,7 +448,10 @@ public class MultiScreen implements Screen {
      */
     public int getWidth() {
         // Return the smallest width of the screens.
-        int width = screens.get(0).getWidth();
+        int width = 80;
+        if (screens.size() > 0) {
+            width = screens.get(0).getWidth();
+        }
         for (Screen screen: screens) {
             if (screen.getWidth() < width) {
                 width = screen.getWidth();
@@ -582,7 +607,10 @@ public class MultiScreen implements Screen {
      * @return true if the cursor is visible
      */
     public boolean isCursorVisible() {
-        return screens.get(0).isCursorVisible();
+        if (screens.size() > 0) {
+            return screens.get(0).isCursorVisible();
+        }
+        return true;
     }
 
     /**
@@ -591,7 +619,10 @@ public class MultiScreen implements Screen {
      * @return the cursor x column position
      */
     public int getCursorX() {
-        return screens.get(0).getCursorX();
+        if (screens.size() > 0) {
+            return screens.get(0).getCursorX();
+        }
+        return 0;
     }
 
     /**
@@ -600,7 +631,10 @@ public class MultiScreen implements Screen {
      * @return the cursor y row position
      */
     public int getCursorY() {
-        return screens.get(0).getCursorY();
+        if (screens.size() > 0) {
+            return screens.get(0).getCursorY();
+        }
+        return 0;
     }
 
     /**
@@ -670,4 +704,70 @@ public class MultiScreen implements Screen {
         return textHeight;
     }
 
+    /**
+     * Invert the cell color at a position, including both halves of a
+     * double-width cell.
+     *
+     * @param x column position
+     * @param y row position
+     */
+    public void invertCell(final int x, final int y) {
+        for (Screen screen: screens) {
+            screen.invertCell(x, y);
+        }
+    }
+
+    /**
+     * Invert the cell color at a position.
+     *
+     * @param x column position
+     * @param y row position
+     * @param onlyThisCell if true, only invert this cell, otherwise invert
+     * both halves of a double-width cell if necessary
+     */
+    public void invertCell(final int x, final int y,
+        final boolean onlyThisCell) {
+
+        for (Screen screen: screens) {
+            screen.invertCell(x, y, onlyThisCell);
+        }
+    }
+
+    /**
+     * Set a selection area on the screen.
+     *
+     * @param x0 the starting X position of the selection
+     * @param y0 the starting Y position of the selection
+     * @param x1 the ending X position of the selection
+     * @param y1 the ending Y position of the selection
+     * @param rectangle if true, this is a rectangle select
+     */
+    public void setSelection(final int x0, final int y0,
+        final int x1, final int y1, final boolean rectangle) {
+
+        for (Screen screen: screens) {
+            screen.setSelection(x0, y0, x1, y1, rectangle);
+        }
+    }
+
+    /**
+     * Copy the screen selection area to the clipboard.
+     *
+     * @param clipboard the clipboard to use
+     * @param x0 the starting X position of the selection
+     * @param y0 the starting Y position of the selection
+     * @param x1 the ending X position of the selection
+     * @param y1 the ending Y position of the selection
+     * @param rectangle if true, this is a rectangle select
+     */
+    public void copySelection(final Clipboard clipboard,
+        final int x0, final int y0, final int x1, final int y1,
+        final boolean rectangle) {
+
+        // Only copy from the first screen.
+        if (screens.size() > 0) {
+            screens.get(0).copySelection(clipboard, x0, y0, x1, y1, rectangle);
+        }
+    }
+
 }
similarity index 87%
rename from src/jexer/backend/Screen.java
rename to backend/Screen.java
index 2a71073176a6608b3a35d0740855e187c746d0e0..a9a2053565b7bf5dc8ba02f25cfce37e9453c714 100644 (file)
@@ -30,6 +30,7 @@ package jexer.backend;
 
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
 
 /**
  * Drawing operations API.
@@ -409,4 +410,50 @@ public interface Screen {
      */
     public int getTextHeight();
 
+    /**
+     * Invert the cell color at a position, including both halves of a
+     * double-width cell.
+     *
+     * @param x column position
+     * @param y row position
+     */
+    public void invertCell(final int x, final int y);
+
+    /**
+     * Invert the cell color at a position.
+     *
+     * @param x column position
+     * @param y row position
+     * @param onlyThisCell if true, only invert this cell, otherwise invert
+     * both halves of a double-width cell if necessary
+     */
+    public void invertCell(final int x, final int y,
+        final boolean onlyThisCell);
+
+    /**
+     * Set a selection area on the screen.
+     *
+     * @param x0 the starting X position of the selection
+     * @param y0 the starting Y position of the selection
+     * @param x1 the ending X position of the selection
+     * @param y1 the ending Y position of the selection
+     * @param rectangle if true, this is a rectangle select
+     */
+    public void setSelection(final int x0, final int y0,
+        final int x1, final int y1, final boolean rectangle);
+
+    /**
+     * Copy the screen selection area to the clipboard.
+     *
+     * @param clipboard the clipboard to use
+     * @param x0 the starting X position of the selection
+     * @param y0 the starting Y position of the selection
+     * @param x1 the ending X position of the selection
+     * @param y1 the ending Y position of the selection
+     * @param rectangle if true, this is a rectangle select
+     */
+    public void copySelection(final Clipboard clipboard,
+        final int x0, final int y0, final int x1, final int y1,
+        final boolean rectangle);
+
 }
similarity index 96%
rename from src/jexer/backend/SwingComponent.java
rename to backend/SwingComponent.java
index 3d1074cf889070d6bc1c7eee4d9be1da80b92d06..df3633398b699585757cab519b1102214983a50c 100644 (file)
@@ -83,7 +83,7 @@ class SwingComponent {
      * Adjustable Insets for this component.  This has the effect of adding a
      * black border around the drawing area.
      */
-    Insets adjustInsets = new Insets(BORDER + 5, BORDER, BORDER, BORDER);
+    Insets adjustInsets = null;
 
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
@@ -96,6 +96,16 @@ class SwingComponent {
      */
     public SwingComponent(final JFrame frame) {
         this.frame = frame;
+        if (System.getProperty("os.name").startsWith("Linux")) {
+            // On my Linux dev system, a Swing frame draws its contents just
+            // a little off.  No idea why, but I've seen it on both Debian
+            // and Fedora with KDE.  These adjustments to the adjustments
+            // seem to center it OK in the frame.
+            adjustInsets = new Insets(BORDER + 5, BORDER,
+                BORDER - 3, BORDER + 2);
+        } else {
+            adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER);
+        }
         setupFrame();
     }
 
@@ -106,6 +116,7 @@ class SwingComponent {
      */
     public SwingComponent(final JComponent component) {
         this.component = component;
+        adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER);
         setupComponent();
     }
 
similarity index 94%
rename from src/jexer/backend/SwingTerminal.java
rename to backend/SwingTerminal.java
index f0ba3552fd52b812a91a06be0f97c9adb96604e7..0727efc894d5832dc515fa1342d622a2eac885df 100644 (file)
@@ -36,6 +36,7 @@ import java.awt.Graphics2D;
 import java.awt.Graphics;
 import java.awt.Insets;
 import java.awt.Rectangle;
+import java.awt.RenderingHints;
 import java.awt.Toolkit;
 import java.awt.event.ComponentEvent;
 import java.awt.event.ComponentListener;
@@ -578,14 +579,12 @@ public class SwingTerminal extends LogicalScreen
         ) {
             do {
                 do {
-                    clearPhysical();
                     drawToSwing();
                 } while (swing.getBufferStrategy().contentsRestored());
 
                 swing.getBufferStrategy().show();
                 Toolkit.getDefaultToolkit().sync();
             } while (swing.getBufferStrategy().contentsLost());
-
         } else {
             // Non-triple-buffered, call drawToSwing() once
             drawToSwing();
@@ -1238,15 +1237,26 @@ public class SwingTerminal extends LogicalScreen
 
         // Draw the background rectangle, then the foreground character.
         assert (cell.isImage());
+
+        // Enable anti-aliasing
+        if (gr instanceof Graphics2D) {
+            ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+            ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING,
+                RenderingHints.VALUE_RENDER_QUALITY);
+        }
+
         gr.setColor(cell.getBackground());
         gr.fillRect(xPixel, yPixel, textWidth, textHeight);
 
         BufferedImage image = cell.getImage();
         if (image != null) {
             if (swing.getFrame() != null) {
-                gr.drawImage(image, xPixel, yPixel, swing.getFrame());
+                gr.drawImage(image, xPixel, yPixel, getTextWidth(),
+                    getTextHeight(), swing.getFrame());
             } else {
-                gr.drawImage(image, xPixel, yPixel, swing.getComponent());
+                gr.drawImage(image, xPixel, yPixel,  getTextWidth(),
+                    getTextHeight(),swing.getComponent());
             }
             return;
         }
@@ -1308,6 +1318,17 @@ public class SwingTerminal extends LogicalScreen
             cellColor.setBackColor(cell.getForeColor());
         }
 
+        // Enable anti-aliasing
+        if ((gr instanceof Graphics2D) && (swing.getFrame() != null)) {
+            // Anti-aliasing on JComponent makes the hash character disappear
+            // for Terminus font, and also kills performance.  Only enable it
+            // for JFrame.
+            ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+            ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING,
+                RenderingHints.VALUE_RENDER_QUALITY);
+        }
+
         // Draw the background rectangle, then the foreground character.
         gr2.setColor(attrToBackgroundColor(cellColor));
         gr2.fillRect(gr2x, gr2y, textWidth, textHeight);
@@ -1740,13 +1761,16 @@ public class SwingTerminal extends LogicalScreen
         } else {
             ch = key.getKeyChar();
         }
-        alt = key.isAltDown();
+        // Both meta and alt count as alt, thanks to Mac using alt for
+        // "symbols" so meta ("command") is the only other modifier left.
+        alt = key.isAltDown() | key.isMetaDown();
         ctrl = key.isControlDown();
         shift = key.isShiftDown();
 
         /*
         System.err.printf("Swing Key: %s\n", key);
         System.err.printf("   isKey: %s\n", isKey);
+        System.err.printf("   meta: %s\n", key.isMetaDown());
         System.err.printf("   alt: %s\n", alt);
         System.err.printf("   ctrl: %s\n", ctrl);
         System.err.printf("   shift: %s\n", shift);
@@ -2101,6 +2125,10 @@ public class SwingTerminal extends LogicalScreen
         boolean eventMouse1 = false;
         boolean eventMouse2 = false;
         boolean eventMouse3 = false;
+        boolean eventAlt = false;
+        boolean eventCtrl = false;
+        boolean eventShift = false;
+
         if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
             eventMouse1 = true;
         }
@@ -2110,6 +2138,16 @@ public class SwingTerminal extends LogicalScreen
         if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
             eventMouse3 = true;
         }
+        if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) {
+            eventAlt = true;
+        }
+        if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) {
+            eventCtrl = true;
+        }
+        if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) {
+            eventShift = true;
+        }
+
         mouse1 = eventMouse1;
         mouse2 = eventMouse2;
         mouse3 = eventMouse3;
@@ -2117,7 +2155,8 @@ public class SwingTerminal extends LogicalScreen
         int y = textRow(mouse.getY());
 
         TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION,
-            x, y, x, y, mouse1, mouse2, mouse3, false, false);
+            x, y, x, y, mouse1, mouse2, mouse3, false, false,
+            eventAlt, eventCtrl, eventShift);
 
         synchronized (eventQueue) {
             eventQueue.add(mouseEvent);
@@ -2145,8 +2184,24 @@ public class SwingTerminal extends LogicalScreen
         oldMouseX = x;
         oldMouseY = y;
 
+        boolean eventAlt = false;
+        boolean eventCtrl = false;
+        boolean eventShift = false;
+
+        int modifiers = mouse.getModifiersEx();
+        if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) {
+            eventAlt = true;
+        }
+        if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) {
+            eventCtrl = true;
+        }
+        if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) {
+            eventShift = true;
+        }
+
         TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION,
-            x, y, x, y, mouse1, mouse2, mouse3, false, false);
+            x, y, x, y, mouse1, mouse2, mouse3, false, false,
+            eventAlt, eventCtrl, eventShift);
 
         synchronized (eventQueue) {
             eventQueue.add(mouseEvent);
@@ -2200,6 +2255,10 @@ public class SwingTerminal extends LogicalScreen
         boolean eventMouse1 = false;
         boolean eventMouse2 = false;
         boolean eventMouse3 = false;
+        boolean eventAlt = false;
+        boolean eventCtrl = false;
+        boolean eventShift = false;
+
         if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
             eventMouse1 = true;
         }
@@ -2209,6 +2268,16 @@ public class SwingTerminal extends LogicalScreen
         if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
             eventMouse3 = true;
         }
+        if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) {
+            eventAlt = true;
+        }
+        if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) {
+            eventCtrl = true;
+        }
+        if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) {
+            eventShift = true;
+        }
+
         mouse1 = eventMouse1;
         mouse2 = eventMouse2;
         mouse3 = eventMouse3;
@@ -2216,7 +2285,8 @@ public class SwingTerminal extends LogicalScreen
         int y = textRow(mouse.getY());
 
         TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN,
-            x, y, x, y, mouse1, mouse2, mouse3, false, false);
+            x, y, x, y, mouse1, mouse2, mouse3, false, false,
+            eventAlt, eventCtrl, eventShift);
 
         synchronized (eventQueue) {
             eventQueue.add(mouseEvent);
@@ -2239,6 +2309,10 @@ public class SwingTerminal extends LogicalScreen
         boolean eventMouse1 = false;
         boolean eventMouse2 = false;
         boolean eventMouse3 = false;
+        boolean eventAlt = false;
+        boolean eventCtrl = false;
+        boolean eventShift = false;
+
         if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
             eventMouse1 = true;
         }
@@ -2248,6 +2322,16 @@ public class SwingTerminal extends LogicalScreen
         if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
             eventMouse3 = true;
         }
+        if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) {
+            eventAlt = true;
+        }
+        if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) {
+            eventCtrl = true;
+        }
+        if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) {
+            eventShift = true;
+        }
+
         if (mouse1) {
             mouse1 = false;
             eventMouse1 = true;
@@ -2264,7 +2348,8 @@ public class SwingTerminal extends LogicalScreen
         int y = textRow(mouse.getY());
 
         TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_UP,
-            x, y, x, y, eventMouse1, eventMouse2, eventMouse3, false, false);
+            x, y, x, y, eventMouse1, eventMouse2, eventMouse3, false, false,
+            eventAlt, eventCtrl, eventShift);
 
         synchronized (eventQueue) {
             eventQueue.add(mouseEvent);
@@ -2293,6 +2378,10 @@ public class SwingTerminal extends LogicalScreen
         boolean eventMouse3 = false;
         boolean mouseWheelUp = false;
         boolean mouseWheelDown = false;
+        boolean eventAlt = false;
+        boolean eventCtrl = false;
+        boolean eventShift = false;
+
         if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
             eventMouse1 = true;
         }
@@ -2302,6 +2391,16 @@ public class SwingTerminal extends LogicalScreen
         if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
             eventMouse3 = true;
         }
+        if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) {
+            eventAlt = true;
+        }
+        if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) {
+            eventCtrl = true;
+        }
+        if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) {
+            eventShift = true;
+        }
+
         mouse1 = eventMouse1;
         mouse2 = eventMouse2;
         mouse3 = eventMouse3;
@@ -2315,7 +2414,8 @@ public class SwingTerminal extends LogicalScreen
         }
 
         TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN,
-            x, y, x, y, mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown);
+            x, y, x, y, mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown,
+            eventAlt, eventCtrl, eventShift);
 
         synchronized (eventQueue) {
             eventQueue.add(mouseEvent);
similarity index 99%
rename from src/jexer/bits/Cell.java
rename to bits/Cell.java
index a8efa2b3c56465dc7ee93dc76fefa399bed85603..ed3c202a005a8c8a2a4187d08478cf8d62a6191c 100644 (file)
@@ -419,7 +419,7 @@ public final class Cell extends CellAttributes {
         int B = 23;
         int hash = A;
         hash = (B * hash) + super.hashCode();
-        hash = (B * hash) + (int)ch;
+        hash = (B * hash) + ch;
         hash = (B * hash) + width.hashCode();
         if (image != null) {
             /*
similarity index 99%
rename from src/jexer/bits/CellAttributes.java
rename to bits/CellAttributes.java
index 99366fda690740b738563493fef900a506436d7d..ad8619896295f00cec4fd0b46608e2082f2b2f53 100644 (file)
@@ -62,7 +62,6 @@ public class CellAttributes {
      */
     private static final int PROTECT    = 0x10;
 
-
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
diff --git a/bits/Clipboard.java b/bits/Clipboard.java
new file mode 100644 (file)
index 0000000..5c1ea9a
--- /dev/null
@@ -0,0 +1,276 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.bits;
+
+import java.awt.Image;
+import java.awt.Toolkit;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.StringSelection;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+
+/**
+ * Clipboard provides convenience methods to copy text and images to and from
+ * a shared clipboard.  When the system clipboard is available it is used.
+ */
+public class Clipboard {
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The image last copied to the clipboard.
+     */
+    private BufferedImage image = null;
+
+    /**
+     * The text string last copied to the clipboard.
+     */
+    private String text = null;
+
+    /**
+     * The system clipboard, or null if it is not available.
+     */
+    private java.awt.datatransfer.Clipboard systemClipboard = null;
+
+    /**
+     * The image selection class.
+     */
+    private ImageSelection imageSelection;
+
+    /**
+     * ImageSelection is used to hold an image while on the clipboard.
+     */
+    private class ImageSelection implements Transferable {
+
+        /**
+         * Returns an array of DataFlavor objects indicating the flavors the
+         * data can be provided in. The array should be ordered according to
+         * preference for providing the data (from most richly descriptive to
+         * least descriptive).
+         *
+         * @return an array of data flavors in which this data can be
+         * transferred
+         */
+        public DataFlavor[] getTransferDataFlavors() {
+            return new DataFlavor[] { DataFlavor.imageFlavor };
+        }
+
+        /**
+         * Returns whether or not the specified data flavor is supported for
+         * this object.
+         *
+         * @param flavor the requested flavor for the data
+         * @return boolean indicating whether or not the data flavor is
+         * supported
+         */
+        public boolean isDataFlavorSupported(DataFlavor flavor) {
+            return DataFlavor.imageFlavor.equals(flavor);
+        }
+
+        /**
+         * Returns an object which represents the data to be transferred. The
+         * class of the object returned is defined by the representation
+         * class of the flavor.
+         *
+         * @param flavor the requested flavor for the data
+         * @throws IOException if the data is no longer available in the
+         * requested flavor.
+         * @throws UnsupportedFlavorException if the requested data flavor is
+         * not supported.
+         */
+        public Object getTransferData(DataFlavor flavor)
+                throws UnsupportedFlavorException, IOException {
+
+            if (!DataFlavor.imageFlavor.equals(flavor)) {
+                throw new UnsupportedFlavorException(flavor);
+            }
+            return image;
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     */
+    public Clipboard() {
+        try {
+            systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+        } catch (java.awt.HeadlessException e) {
+            // SQUASH
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // Clipboard --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Copy an image to the clipboard.
+     *
+     * @param image image to copy
+     */
+    public void copyImage(final BufferedImage image) {
+        this.image = image;
+        if (systemClipboard != null) {
+            ImageSelection imageSelection = new ImageSelection();
+            systemClipboard.setContents(imageSelection, null);
+        }
+    }
+
+    /**
+     * Copy a text string to the clipboard.
+     *
+     * @param text string to copy
+     */
+    public void copyText(final String text) {
+        this.text = text;
+        if (systemClipboard != null) {
+            StringSelection stringSelection = new StringSelection(text);
+            systemClipboard.setContents(stringSelection, null);
+        }
+    }
+
+    /**
+     * Obtain an image from the clipboard.
+     *
+     * @return image from the clipboard, or null if no image is available
+     */
+    public BufferedImage pasteImage() {
+        if (systemClipboard != null) {
+            getClipboardImage();
+        }
+        return image;
+    }
+
+    /**
+     * Obtain a text string from the clipboard.
+     *
+     * @return text string from the clipboard, or null if no text is
+     * available
+     */
+    public String pasteText() {
+        if (systemClipboard != null) {
+            getClipboardText();
+        }
+        return text;
+    }
+
+    /**
+     * Returns true if the clipboard has an image.
+     *
+     * @return true if an image is available from the clipboard
+     */
+    public boolean isImage() {
+        if (image == null) {
+            getClipboardImage();
+        }
+        return (image != null);
+    }
+
+    /**
+     * Returns true if the clipboard has a text string.
+     *
+     * @return true if a text string is available from the clipboard
+     */
+    public boolean isText() {
+        if (text == null) {
+            getClipboardText();
+        }
+        return (text != null);
+    }
+
+    /**
+     * Returns true if the clipboard is empty.
+     *
+     * @return true if the clipboard is empty
+     */
+    public boolean isEmpty() {
+        return ((isText() == false) && (isImage() == false));
+    }
+
+    /**
+     * Copy image from the clipboard to text.
+     */
+    private void getClipboardImage() {
+        if (systemClipboard != null) {
+            Transferable contents = systemClipboard.getContents(null);
+            if (contents != null) {
+                if (contents.isDataFlavorSupported(DataFlavor.imageFlavor)) {
+                    try {
+                        Image img = (Image) contents.getTransferData(DataFlavor.imageFlavor);
+                        image = new BufferedImage(img.getWidth(null),
+                            img.getHeight(null), BufferedImage.TYPE_INT_ARGB);
+                        image.getGraphics().drawImage(img, 0, 0, null);
+                    } catch (IOException e) {
+                        // SQUASH
+                    } catch (UnsupportedFlavorException e) {
+                        // SQUASH
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Copy text string from the clipboard to text.
+     */
+    private void getClipboardText() {
+        if (systemClipboard != null) {
+            Transferable contents = systemClipboard.getContents(null);
+            if (contents != null) {
+                if (contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
+                    try {
+                        text = (String) contents.getTransferData(DataFlavor.stringFlavor);
+                    } catch (IOException e) {
+                        // SQUASH
+                    } catch (UnsupportedFlavorException e) {
+                        // SQUASH
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Clear whatever is on the local clipboard.  Note that this will not
+     * clear the system clipboard.
+     */
+    public void clear() {
+        image = null;
+        text = null;
+    }
+
+}
similarity index 100%
rename from src/jexer/bits/Color.java
rename to bits/Color.java
similarity index 92%
rename from src/jexer/bits/ColorTheme.java
rename to bits/ColorTheme.java
index ffba4d472cc67c36ebda4114e503ba1db545c719..3efce633243c9f82433d0212ad13e6caefe4922c 100644 (file)
@@ -178,8 +178,11 @@ public class ColorTheme {
             return;
         }
 
-        while (token.equals("bold") || token.equals("blink")) {
-            if (token.equals("bold")) {
+        while (token.equals("bold")
+            || token.equals("bright")
+            || token.equals("blink")
+        ) {
+            if (token.equals("bold") || token.equals("bright")) {
                 bold = true;
                 token = tokenizer.nextToken();
             }
@@ -231,8 +234,8 @@ public class ColorTheme {
                 // Invalid line.
                 continue;
             }
-            String key = line.substring(0, line.indexOf(':')).trim();
-            String text = line.substring(line.indexOf(':') + 1);
+            String key = line.substring(0, line.indexOf('=')).trim();
+            String text = line.substring(line.indexOf('=') + 1);
             setColorFromString(key, text);
         }
         // All done.
@@ -633,6 +636,11 @@ public class ColorTheme {
         color.setBackColor(Color.BLUE);
         color.setBold(false);
         colors.put("teditor", color);
+        color = new CellAttributes();
+        color.setForeColor(Color.BLACK);
+        color.setBackColor(Color.CYAN);
+        color.setBold(false);
+        colors.put("teditor.selected", color);
 
         // TTable
         color = new CellAttributes();
@@ -673,6 +681,48 @@ public class ColorTheme {
         color.setBold(false);
         colors.put("tsplitpane", color);
 
+        // THelpWindow border - during window movement
+        color = new CellAttributes();
+        color.setForeColor(Color.GREEN);
+        color.setBackColor(Color.CYAN);
+        color.setBold(true);
+        colors.put("thelpwindow.windowmove", color);
+
+        // THelpWindow border
+        color = new CellAttributes();
+        color.setForeColor(Color.GREEN);
+        color.setBackColor(Color.CYAN);
+        color.setBold(true);
+        colors.put("thelpwindow.border", color);
+
+        // THelpWindow background
+        color = new CellAttributes();
+        color.setForeColor(Color.WHITE);
+        color.setBackColor(Color.CYAN);
+        color.setBold(true);
+        colors.put("thelpwindow.background", color);
+
+        // THelpWindow text
+        color = new CellAttributes();
+        color.setForeColor(Color.WHITE);
+        color.setBackColor(Color.BLUE);
+        color.setBold(false);
+        colors.put("thelpwindow.text", color);
+
+        // THelpWindow link
+        color = new CellAttributes();
+        color.setForeColor(Color.YELLOW);
+        color.setBackColor(Color.BLUE);
+        color.setBold(true);
+        colors.put("thelpwindow.link", color);
+
+        // THelpWindow link - active
+        color = new CellAttributes();
+        color.setForeColor(Color.YELLOW);
+        color.setBackColor(Color.CYAN);
+        color.setBold(true);
+        colors.put("thelpwindow.link.active", color);
+
     }
 
     /**
similarity index 61%
rename from src/jexer/bits/StringUtils.java
rename to bits/StringUtils.java
index fffce206875cf663480d2041aac121f88b58d01a..d33f71f4e0031710e52ed008fefa0f37f28883c4 100644 (file)
@@ -30,6 +30,7 @@ package jexer.bits;
 
 import java.util.List;
 import java.util.ArrayList;
+import java.util.Arrays;
 
 /**
  * StringUtils contains methods to:
@@ -41,6 +42,11 @@ import java.util.ArrayList;
  *
  *    - Read/write a line of RFC4180 comma-separated values strings to/from a
  *      list of strings.
+ *
+ *    - Compute number of visible text cells for a given Unicode codepoint or
+ *      string.
+ *
+ *    - Convert bytes to and from base-64 encoding.
  */
 public class StringUtils {
 
@@ -466,6 +472,10 @@ public class StringUtils {
      * @return the number of text cell columns required to display this string
      */
     public static int width(final String str) {
+        if (str == null) {
+            return 0;
+        }
+
         int n = 0;
         for (int i = 0; i < str.length();) {
             int ch = str.codePointAt(i);
@@ -495,4 +505,241 @@ public class StringUtils {
         return ((ch >= 0x1f004) && (ch <= 0x1fffd));
     }
 
+    // ------------------------------------------------------------------------
+    // Base64 -----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /*
+     * The Base64 encoder/decoder below is provided to support JDK 1.6 - JDK
+     * 11.  It was taken from https://sourceforge.net/projects/migbase64/
+     *
+     * The following changes were made:
+     *
+     * - Code has been indented and long lines cut to fit within 80 columns.
+     *
+     * - Char, String, and "fast" byte functions removed.  byte versions
+     *   retained and called toBase64()/fromBase64().
+     *
+     * - Enclosing braces added to blocks.
+     */
+
+    /**
+     * A very fast and memory efficient class to encode and decode to and
+     * from BASE64 in full accordance with RFC 2045.<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 (&lt 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;
+    }
+
 }
diff --git a/build.xml b/build.xml
deleted file mode 100644 (file)
index 92d488b..0000000
--- a/build.xml
+++ /dev/null
@@ -1,125 +0,0 @@
-<!--
-
-   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 &#169; 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>
similarity index 100%
rename from src/jexer/demos/Demo1.java
rename to demos/Demo1.java
similarity index 100%
rename from src/jexer/demos/Demo2.java
rename to demos/Demo2.java
similarity index 100%
rename from src/jexer/demos/Demo3.java
rename to demos/Demo3.java
similarity index 100%
rename from src/jexer/demos/Demo4.java
rename to demos/Demo4.java
similarity index 100%
rename from src/jexer/demos/Demo5.java
rename to demos/Demo5.java
similarity index 99%
rename from src/jexer/demos/Demo6.java
rename to demos/Demo6.java
index db0b5c9d3f53a7083d4a47a99930ebcec9058d3b..41d1f2c3f323b84a4feaa583534a34c793f06344 100644 (file)
@@ -111,7 +111,7 @@ public class Demo6 {
              * Make a new Swing window for the second application.
              */
             SwingBackend monitorBackend = new SwingBackend(width + 5,
-                height + 5, 16);
+                height + 5, 20);
 
             /*
              * Setup the second application, give it the basic file and
similarity index 100%
rename from src/jexer/demos/Demo7.java
rename to demos/Demo7.java
diff --git a/demos/Demo8.java b/demos/Demo8.java
new file mode 100644 (file)
index 0000000..19fe5ff
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.demos;
+
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.text.MessageFormat;
+import java.util.ResourceBundle;
+
+import jexer.TApplication;
+import jexer.backend.*;
+import jexer.demos.DemoApplication;
+import jexer.net.TelnetServerSocket;
+
+
+/**
+ * This class shows off the use of MultiBackend and MultiScreen.
+ */
+public class Demo8 {
+
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(Demo8.class.getName());
+
+    // ------------------------------------------------------------------------
+    // Demo8 ------------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Main entry point.
+     *
+     * @param args Command line arguments
+     */
+    public static void main(final String [] args) {
+        ServerSocket server = null;
+        try {
+
+            /*
+             * In this demo we will create a headless application that anyone
+             * can telnet to.
+             */
+
+            /*
+             * Check the arguments for the port to listen on.
+             */
+            if (args.length == 0) {
+                System.err.println(i18n.getString("usageString"));
+                return;
+            }
+            int port = Integer.parseInt(args[0]);
+
+            /*
+             * We create a headless screen and use it to establish a
+             * MultiBackend.
+             */
+            HeadlessBackend headlessBackend = new HeadlessBackend();
+            MultiBackend multiBackend = new MultiBackend(headlessBackend);
+
+            /*
+             * Now we create the shared application (a standard demo) and
+             * spin it up.
+             */
+            DemoApplication demoApp = new DemoApplication(multiBackend);
+            (new Thread(demoApp)).start();
+            multiBackend.setListener(demoApp);
+
+            /*
+             * Fire up the telnet server.
+             */
+            server = new TelnetServerSocket(port);
+            while (demoApp.isRunning()) {
+                Socket socket = server.accept();
+                System.out.println(MessageFormat.
+                    format(i18n.getString("newConnection"), socket));
+
+                ECMA48Backend ecmaBackend = new ECMA48Backend(demoApp,
+                    socket.getInputStream(),
+                    socket.getOutputStream());
+
+                /*
+                 * Add this screen to the MultiBackend, and at this point we
+                 * have the telnet client able to use the shared demo
+                 * application.
+                 */
+                multiBackend.addBackend(ecmaBackend);
+
+                /*
+                 * Emit the connection information from telnet.
+                 */
+                Thread.sleep(500);
+                System.out.println(MessageFormat.
+                    format(i18n.getString("terminal"),
+                    ((jexer.net.TelnetInputStream) socket.getInputStream()).
+                        getTerminalType()));
+                System.out.println(MessageFormat.
+                    format(i18n.getString("username"),
+                    ((jexer.net.TelnetInputStream) socket.getInputStream()).
+                        getUsername()));
+                System.out.println(MessageFormat.
+                    format(i18n.getString("language"),
+                    ((jexer.net.TelnetInputStream) socket.getInputStream()).
+                        getLanguage()));
+
+            } // while (demoApp.isRunning())
+
+            /*
+             * When the application exits, kill all of the connections too.
+             */
+            multiBackend.shutdown();
+            server.close();
+
+            System.out.println(i18n.getString("exitMain"));
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (server != null) {
+                try {
+                    server.close();
+                } catch (Exception e) {
+                    // SQUASH
+                }
+            }
+        }
+    }
+
+}
diff --git a/demos/Demo8.properties b/demos/Demo8.properties
new file mode 100644 (file)
index 0000000..08a8217
--- /dev/null
@@ -0,0 +1,6 @@
+usageString=USAGE: java -cp jexer.jar jexer.demos.Demo8 port
+newConnection=New connection: {0}
+username=\ \ \ username: {0}
+language=\ \ \ language: {0}
+terminal=\ \ \ terminal: {0}
+exitMain=Main thread is exiting...
similarity index 98%
rename from src/jexer/demos/DemoCheckBoxWindow.java
rename to demos/DemoCheckBoxWindow.java
index fda7bd7a15ae6567eee7a21d17b090cc46648c4f..faf3530cd47e86da9fac5103aac5e7dd6303dcba 100644 (file)
@@ -103,8 +103,9 @@ public class DemoCheckBoxWindow extends TWindow {
         TRadioGroup group = addRadioGroup(1, row,
             i18n.getString("radioGroupTitle"));
         group.addRadioButton(i18n.getString("radioOption1"));
-        group.addRadioButton(i18n.getString("radioOption2"));
+        group.addRadioButton(i18n.getString("radioOption2"), true);
         group.addRadioButton(i18n.getString("radioOption3"));
+        group.setRequiresSelection(true);
 
         List<String> comboValues = new ArrayList<String>();
         comboValues.add(i18n.getString("comboBoxString0"));
diff --git a/docs/032_announcement.txt b/docs/032_announcement.txt
deleted file mode 100644 (file)
index ee2d5fc..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-Jexer 0.3.2 Release
-===================
-
-I am pleased to announce the release of Jexer 0.3.2.  This release
-completes nearly every feature I set out to make, and is the last
-major milestone before 1.0.0.
-
-Jexer is not an application itself, but rather an advanced text
-windowing system framework to help new applications take full
-advantage of the terminal.  Its major features are:
-
-  * MIT licensed.
-
-  * Direct support for xterm-like terminals: mouse, keyboard, 24-bit
-    RGB color, UTF-8, fullwidth characters (CJK and emoji), and sixel
-    images.
-
-  * A Swing-based GUI window that ships with a good-looking Terminus
-    font.
-
-  * Sixel image support, for both input in its terminal window and
-    output to the host terminal.  Jexer is (to my knowledge) the first
-    and only system capable of managing multiple terminal windows
-    displaying properly overlapping images.
-
-  * Draggable / resizable windows, menu bar, and system-modal dialogs
-    (message/input boxes and filename picker).
-
-  * A full complement of widgets: button, text field, checkbox,
-    combobox, list, radio button, scrollbars, data table, calendar
-    picker, progress bar, text display, and simple text editor.  Plus
-    layout manager support for resizable widgets and windows.
-
-  * A terminal window capable of passing "vttest" (including VT100
-    double-width / double-height), and supporting all of Jexer's
-    features.  Jexer can run inside itself, with full keyboard, mouse,
-    and image support.
-
-  * Extensively documented in the code (Javadoc), a wiki, and ships
-    with a demonstration application showing off all of its available
-    widgets.
-
-
-Find out more at the Jexer Sourceforge or GitLab project pages:
-
-  * https://jexer.sourceforge.io/
-
-  * https://gitlab.com/klamonte/jexer
-
-
-Download
---------
-
-GitLab: git clone https://gitlab.com/klamonte/jexer.git
-
-Binary downloads: http://sourceforge.net/project/showfiles.php?group_id=2829121
-
-On Maven:
-
-    group: com.gitlab.klamonte
-    artifact: jexer
-    version: 0.3.2
-
-
-Ugh, Java Sucks!
-----------------
-
-(Thor squint) But does it though?
-
-More seriously, I initially picked D because it was sexy.  But D circa
-2013 brought too many headaches for me, so I switched to Java because
-I wanted a cross-platform standard library that would be stable over
-many years.  And Java is OK, it is a solid workhorse that gets the job
-done.
-
-Yet in porting my initial work to Java I stumbled upon an unexpected
-benefit: I found ways to accomplish all of what Jexer does _without
-calling C directly_.  No termios, no ncurses, no forkpty(), and thus
-no serious hurdles porting it to anything that can spawn programs and
-read their output.  On Linux, BSD, or OSX, all you need is 'stty' and
-'script' to make things work.  (And if you want resizable terminal
-windows, add 'ptypipe'.)
-
-So for those who want something like Jexer but in your own favorite
-language, I encourage you to check out the [Porting
-Jexer](https://gitlab.com/klamonte/jexer/wikis/porting) page on the
-wiki: it has pointers to where the key features are, and a potential
-roadmap if you wanted to take part or all of it into your own hands.
-I licensed Jexer as MIT, stuck with simple Java 1.6, and thoroughly
-documented it in the hope that fans of other languages could more
-easily create or enhance their own text user interfaces.
diff --git a/docs/images.md b/docs/images.md
deleted file mode 100644 (file)
index 919f63d..0000000
+++ /dev/null
@@ -1,1252 +0,0 @@
-Terminal Emulator Multimedia Standard - Proposed Design
-=======================================================
-
-Version: 1
-
-
-
-Purpose
--------
-
-Multiple standards exist to incorporate image data in text-based
-terminals and terminal emulators.  Few standards have wide adoption
-despite frequent user requests for these features and hardware support
-for several of the standards.
-
-A group including developers of several widely-used terminal emulators
-has been working on defining the needs and limitations for a standard
-that can be implemented in current-gen terminal emulators.  The
-discussion has been primarily captured here:
-https://gitlab.freedesktop.org/terminal-wg/specifications/issues/12
-
-This document collects many of the reported desires and practical
-constraints of that discussion into a proposed standard that
-encompasses three independent new features:
-
-1. A method to transfer multimedia data for immediate display within
-   the screen cell grid ("Direct Multimedia").
-
-2. A method to transfer multimedia data to a terminal-managed cache,
-   and later display that data within the screen cell grid ("Cached
-   Multimedia").
-
-3. A method to assign cell data to different layers with options for
-   both layer and cell transparency ("Layers").
-
-A terminal may implement any combination of these features
-independently of each other.  If all features are supported, then all
-of the design goals outlined in this document can be met.
-
-The same mechanisms that can put raster-based images on the screen are
-also readily generalizable to other media types such as vector-based
-images and animations.  This document is thus a "multimedia" proposal
-rather than a "simple images" proposal.
-
-
-
-Acknowledgements
-----------------
-
-This proposal has been informed from the following prior work:
-
-* DEC VT300 series sixel graphics standard:
-  https://vt100.net/docs/vt3xx-gp/chapter14.html
-
-* iTerm2 image protocol:
-  https://iterm2.com/documentation-images.html
-
-* Kitty image protocol:
-  https://sw.kovidgoyal.net/kitty/graphics-protocol.html
-
-* Jexer Terminal User Interface:
-  https://gitlab.com/klamonte/jexer
-
-
-
-Design Goals - Core
--------------------
-
-The core ("must-have") design goals are:
-
-* Be easy to implement in existing terminals and applications:
-
-  - Sacrifice "10%" of potential function to eliminate "90%" of
-    implementation pain.  "Less is more."
-
-  - Be a strict superset of the existing iTerm2 and DEC sixel image
-    solutions.  One should be able to take an existing terminal or
-    application that emits/consumes iTerm2 or sixel sequences, and
-    only change the control sequence introducer/termination to achieve
-    the same effect as a terminal/application that conforms with this
-    standard.
-
-* Have no ambiguity.  If two terminal or application developers can
-  read this document and reach different conclusions on what should be
-  on the screen, then an error exists in this document that must be
-  corrected.
-
-  - Every feature must be straightforward to validate via automated
-    unit testing.
-
-  - Every conformant terminal must produce the same output (pixels on
-    screen) given the same input (terminal font, terminal sequences).
-
-  - Every option must have a defined default value.
-
-  - Erroneous sequences must have defined expected results.
-
-  - Every operation must act atomically: either everything worked
-    (image is on screen, cursor has moved, terminal state has changed,
-    etc.) or nothing did.
-
-* Integrate with existing ECMA-48 / ANSI X3.64 defined sequences:
-
-  - Operations on Tiles/Cells containing text will have the same
-    effect when applied to Tiles/Cells containing image data.
-
-  - Existing sequences are given new parameters to cover needed
-    features rather than entirely new sequences introduced.
-
-* Be straightforward to implement in non-"physical" terminals,
-  including:
-
-  - Future versions of terminal control libraries such as ncurses and
-    termbox.
-
-  - Terminal multiplexers that support "headless" terminals (no
-    physical screen) and "multi-head" terminals (many different
-    physical screens).
-
-* Be platform-agnostic, and easy to implement on (at the least):
-  POSIX, Windows, and web.
-
-  - All features must be available even if the only means of
-    communication between the application and terminal is control
-    sequences (e.g. no shared disk, no shared memory, no shared DOM,
-    etc.).
-
-* Support graceful fallback:
-
-  - Terminal emulators and physical terminals that do not support this
-    standard should remain usable with no undefined screen artifacts,
-    even when the application blindly emits these sequences to those
-    terminals.
-
-  - This standard must able to be versioned for future enhancements.
-
-  - An application must be able to detect that its terminal supports
-    this standard, and at what version.
-
-* Support secure programming practices:
-
-  - Applications must not be able to obtain unauthorized data from
-    terminal memory, such as: images emitted by other applications
-    still present in the terminal's scrollback buffer, terminal or
-    system memory limits.
-
-  - Applications must not be able to compromise the terminal through
-    denial-of-service such as: excessive memory usage, unterminated
-    control sequences.  Similarly, terminals must not be able to
-    compromise application through their responses to application
-    queries.
-
-  - Applications must not be able to manipulate the terminal into
-    performing an insecure operation such as: reading arbitrary shared
-    memory regions, reading arbitrary files on disk, deleting
-    arbitrary files on disk, etc.  Similarly, terminals must not be
-    able to manipulate applications into performing insecure
-    operations.
-
-  - This standard must be implementable when the terminal has a fixed
-    maximum memory, such as a kernel-level device driver.
-
-
-
-Design Goals - Secondary
-------------------------
-
-The secondary ("nice-to-have") design goals are listed below.  These
-might not all be possible, but will kept in mind:
-
-* Minimal redundant network traffic for on-screen data that is
-  repeated: either on screen in multiple places, or in the same place
-  but refreshed multiple times.
-
-* Asynchronous notification from terminal to application that the
-  screen has been changed by outside or user action.  Examples: font
-  change, session detach/attach, user changed image preferences.
-
-* The ability for a multiplexer to "pass-thru" the image drawing
-  sequence to its "outer" terminal, with some support for limited
-  clipping.
-
-
-
-Out Of Scope
-------------
-
-The following items are out of scope:
-
-* Bidirectional output.  Applications are expected to generate Tiles
-  and place them on screen where they need.  The cursor response to
-  image sequences are defined as left-to-right-top-to-bottom,
-  consistent with ECMA-48 / ANSI X3.64 sequences.  An independent BIDI
-  standard is free to apply whatever solution will work for ECMA-48 /
-  ANSI X3.64 sequences to the sequences described in this document.
-
-* Capabilities.  This standard defines a limited number of new
-  terminal reports and responses.  These are not intended to be used
-  as a general-purpose capabilities model.
-
-* Terminal Cache Management.  This standard defines a means for
-  applications and terminals to communicate around cached multimedia
-  items, but terminals are free to implement whatever cache management
-  strategies they deem fit.
-
-* Reliable Transport.  This standard defines a two-way
-  command/response protocol that may get out of order on unreliable
-  channels such as 3-wire RS232.  Applictions that require reliable
-  transport on unreliable links may choose to use one of the many
-  successful standards available for this purpose.
-
-
-
-Definitions
------------
-
-Terminal - The hardware, or a program that simulates hardware,
-           comprising a keyboard, screen, and mouse.
-
-Application - A program that utilizes the terminal for its
-              input/output with the user.
-
-Multiplexer - A special case of an application that simulates one or
-              more "inner" terminals for other applications to use,
-              and composes these inner terminals into a combined
-              screen to emit to one or more "outer" terminals that
-              obtain input/output from the user.  Multiplexers are
-              thus both applications and terminals.
-
-X - The column coordinate of a cell.  This standard is 1-based (like
-    ECMA-48): the left-most column of the screen is numbered 1.
-
-Y - The row coordinate of a cell.  This standard is 1-based (like
-    ECMA-48): the top-most row of the screen is numbered 1.
-
-Z - The layer that text or multimedia is placed on.  This proposal
-    uses a right-hand coordinate system with (X, Y, Z) = (1, 1, 1)
-    defined as the top-left corner on the default layer; positive Z
-    projects "away" from the user and "into" or "behind" the screen.
-    Rendering the Cells on the screen must produce the same result as
-    painter's algorithm (see "Layers - Rendering" section below).
-
-Cell - A fixed-width-and-height rectangle on the screen.  The cells of
-       the screen are arranged in a grid of X columns and Y rows.  A
-       Cell has dimensions of cellWidth and cellHeight pixels.  Every
-       Cell has a coordinate of (X, Y) (or (X, Y, Z) when the terminal
-       supports the layers feature).
-
-Tile - One or more contiguous Cells with data to be displayed.  The
-       data can be text or image data, but not both.  A Tile has width
-       of 1, 2, or more, and a coordinate of (X, Y, Z) that is the
-       same as its left-most (first) Cell's (X, Y, Z).  In practice,
-       Tiles are typically one Cell wide for ASCII and Latin language
-       glyphs, and two Cells wide for "fullwidth" glyphs as used in
-       Asian langauges, emojis, and symbols.  This standard does not
-       preclude Tiles from encompassing entire grapheme clusters.
-       Note that ECMA-48 / ANSI X3.64 operations are performed against
-       Tiles, not Cells: if a 2-Cell-wide Tile is deleted via
-       backspace, then the cursor will decrement on screen by two
-       columns.
-
-Layer - A screen-sized grid of Cells that have the same Z coordinate.
-        Layers are drawn to the screen in descending Z order.  Layers
-        may have optional additional attributes such as transparency.
-        Layer support is an orthogonal (independent) option to
-        multimedia support.  It is acceptable for terminals to support
-        multimedia without layers and vice versa.
-
-
-
-All Features - Detection
-------------------------
-
-Applications can detect support for these features using Primary
-Device Attributes (DA) and DECID (ESC Z, or 0x9A).
-
-Terminals that support this standard will repond with additional
-parameter(s): "224" for direct multimedia, "225" for cached
-multimedia, and "226" for layers.  A recap of the parameters xterm
-supports is listed below, with these new feature responses included:
-
-| VT220 (and higher) Response | Description                                |
-|-----------------------------|--------------------------------------------|
-| 1                           | 132-columns                                |
-| 2                           | Printer                                    |
-| 3                           | ReGIS graphics                             |
-| 4                           | Sixel graphics                             |
-| 6                           | Selective erase                            |
-| 8                           | User-defined keys                          |
-| 9                           | National Replacement Character sets        |
-| 1 5                         | Technical characters                       |
-| 1 6                         | Locator port                               |
-| 1 7                         | Terminal state interrogation               |
-| 1 8                         | User windows                               |
-| 2 1                         | Horizontal scrolling                       |
-| 2 2                         | ANSI color, e.g., VT525                    |
-| 2 8                         | Rectangular editing                        |
-| 2 9                         | ANSI text locator (i.e., DEC Locator mode) |
-| 2 2 4                       | Direct Multimedia Version 1                |
-| 2 2 5                       | Cached Multimedia Version 1                |
-| 2 2 6                       | Layers                                     |
-
-
-
-Direct Multimedia - Summary
----------------------------
-
-Non-text data (multimedia) can be sent to the terminal for immediate
-display in a rectangular (single-layer) region of text Cells.
-Multimedia data is transmitted to the terminal using one of two wire
-formats described later in this document.
-
-Setting a Cell to multimedia is a destructive operation: the Cell's
-original text is lost.  Multimedia pixels will not overlap rendered
-text in the same Cell.  To achieve pixels overlaid on text, the layers
-feature can be used.
-
-Setting any part of a multi-Cell Tile to multimedia also "breaks up"
-the Tile into a range of single Cells.  In other words, multimedia can
-only be carried by a Cell, not a Tile.
-
-The pixels of a multimedia Cell are assigned to the Cell's foreground;
-multimedia Cells have no background.  If a terminal supports the
-layers feature, setting a multimedia Cell's foreground transparency to
-true/enabled causes that Cell to not be displayed at all; setting its
-background transparency to either true/enabled or false/disabled has
-no visible effect.
-
-The pixels of multimedia Cells can come from two sources:
-
-  1. The application can generate pixels and send them to the terminal
-     for display at the current cursor position.
-
-  2. The application can specify a source for the multimedia and the
-     terminal will generate the pixels for display at the current
-     cursor position.
-
-
-
-Direct Multimedia - Required Support For Existing Sequences
------------------------------------------------------------
-
-A terminal with direct multimedia feature must support the following
-defined xterm sequences:
-
-| Sequence       | Description                                         |
-|----------------|-----------------------------------------------------|
-| CSI 16 t       | Responds with CSI 6 ; cellHeight ; cellWidth t      |
-| CSI 18 t       | Responds with CSI 8 ; rows ; columns t              |
-
-
-
-Direct Multimedia - New Sequences
----------------------------------
-
-A terminal with direct multimedia feature must support the following
-new sequences:
-
-| Sequence                             | Command     | Description             |
-|--------------------------------------|-------------|-------------------------|
-| OSC 1 3 3 8 ; s i x e l : {data} BEL | SIXEL       | Display sixel at (x, y) |
-| OSC 1 3 3 8 ; s i x e l : {data} ST  | SIXEL       | Display sixel at (x, y) |
-| OSC 1 3 3 8 ; F i l e = {args} : {data} BEL   | DMDISPLAY   | Display media at (x, y) |
-| OSC 1 3 3 8 ; F i l e = {args} : {data} ST    | DMDISPLAY   | Display media at (x, y) |
-| CSI ? 3 0 0 0 h               | DECSET 3000 | Enable SCRCHANGE notification  |
-| CSI ? 3 0 0 0 l               | DECRST 3000 | Disable SCRCHANGE notification |
-| OSC 1 3 3 9 ; Pe ; {args} ST  | DMRESP      | Terminal response to DMDISPLAY |
-| CSI ? 3 0 0 1 h               | DECSET 3001 | Enable DMDISPLAY responses     |
-| CSI ? 3 0 0 1 l               | DECRST 3001 | Disable DMDISPLAY responses    |
-
-
-
-If SCRCHANGE is set/enabled, then the terminal will send the "CSI 6 ;
-cellHeight ; cellWidth t" when the font size has changed, and "CSI 8 ;
-rows ; columns t" when the number of rows/columns on the screen has
-changed.
-
-
-
-For the SIXEL command:
-
-* The {data} is a sixel sequence as described in the VT330/340
-  Programmer Reference Manual, Chapter 14, available online at:
-  http://vt100.net/docs/vt3xx-gp/chapter14.html .  The {data} is the
-  "P1 ; P2 ; P3 ; q s..s" portion of the Device Control String, i.e. a
-  complete sixel sequence minus the leading DCS and trailing ST.
-
-* The sixel image is processed as shown below.  Note that this
-  behavior is equivalent to Sixel Scrolling mode enabled.
-
-  - The sixel active position starts at the upper-left corner of the
-    text cursor position.
-
-  - The screen is scrolled up if the image overflows into the bottom
-    text row.
-
-  - Pixels that would be drawn to the right of the visible region on
-    screen are discarded.
-
-  - The cursor's final position is on the same column as the starting
-    cursor position, and on the row immediately below the image.
-
-
-For the DMDISPLAY command:
-
-* The {args} is a set of key-value pairs (each pair separated by
-  semicolon (';')), followed by a colon (':'), followed by a base-64
-  encoded string ({data}).
-
-* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z',
-  'a' - 'z').
-
-* A value is any printable ASCII string not containing whitespace,
-  colon, or semicolon ('!' - '9', '<' - '~').
-
-* Any alpha-numeric key may be specified.  A key that is not supported
-  by the terminal is ignored without error.
-
-* The multimedia pixels are processed as shown below.
-
-  - The pixel are drawn starting at the upper-left corner of the text
-    cursor position.
-
-  - If scroll is specified as 1 (enabled), then:
-
-    a. The screen is scrolled up if the image overflows into the
-       bottom text row.
-
-    b. The cursor's final position is on the same column as the
-       starting cursor position, and on the row immediately below the
-       image.
-
-  - If scroll is omitted or specified as 0 (disabled), then:
-
-    a. The screen is never scrolled.
-
-    b. Pixels that would be drawn below the visible region on screen
-       are discarded.
-
-    c. The cursor's final position is at the same column and row as
-       the starting cursor position, i.e. the cursor does not move at
-       all.
-
-  - Pixels that would be drawn to the right of the visible region on
-    screen are discarded.
-
-
-
-The keys for the key-value pairs that must be supported by the
-terminal are listed below:
-
-| Key          | Default Value | Description                                  |
-|--------------|---------------|----------------------------------------------|
-| type         | "image/rgb"   | mime-type describing data field              |
-| url          | ""            | If set, a location containing the media data |
-| width        | 1             | Number of Cells or pixels wide to display in |
-| height       | 1             | Number of Cells or pixels high to display in |
-| scale        | "none"        | Scale/zoom option, see below                 |
-| align        | "nw"          | Align image to edge option, see below        |
-| sourceX      | 0             | Media source X position to display           |
-| sourceY      | 0             | Media source Y position to display           |
-| sourceWidth  | "auto"        | Media width in pixels to display             |
-| sourceHeight | "auto"        | Media height in pixels to display            |
-| scroll       | 1             | If 1, scroll the display if needed           |
-
-A terminal may support additional keys.  If a key is specified but not
-supported by the terminal, then it is ignored without error.
-
-
-
-The "type" value is a mime-type string describing the format of the
-base64-encoded binary data.  The terminal must support at mimunum these
-mime-types:
-
-| Type String   | Description                                                  |
-|---------------|--------------------------------------------------------------|
-| "image/rgb"   | Big-endian-encoded 24-bit red, green, blue values            |
-| "image/rgba"  | Big-endian-encoded 32-bit red, green, blue, alpha values     |
-| "image/png"   | PNG file data as described by (reference to PNG format)      |
-
-A terminal may support additional types.  An application can detect
-terminal support for a format by: enabling terminal responses (DECSET
-3001), sending a DMDISPLAY command, and examining the terminal's
-response sequence for success or error.
-
-
-
-The "url" value is a RFC-XXXX defined Universal Resource Located,
-encoded in RFC-XXXX form as a printable ASCII string not containing:
-whitespace, colon (':'), semicolon (';'), or equals ('=').
-
-A terminal is not required to support any URLs.
-
-
-
-The "width" and "height" values can take the following forms:
-
-| Value                         | Meaning                   |
-|-------------------------------|---------------------------|
-| N (a positive integer)        | Number of Cells           |
-| Npx (positive integer + "px") | Number of pixels          |
-| N% (positive integer + "%")   | Percent of screen width or height |
-| "auto"                        | Number of pixels as defined by the multimedia data |
-
-
-
-The "scale" value can take the following values:
-
-| Value      | Meaning                                                       |
-|------------|---------------------------------------------------------------|
-| "none"     | No scaling along either axis.                                 |
-| "scale"    | Stretch image, preserving aspect ratio, to maximum size in the target area without cropping |
-| "stretch"  | Stretch along both axes, distorting aspect ratio, to fill the target area               |
-| "crop"     | Stretch along both axes, preserving aspect ration, to completely fill the target area, cropping pixels that will not fit |
-
-
-
-The "align" value can take the following values:
-
-| Value      | Meaning                                                         |
-|------------|-----------------------------------------------------------------|
-| "nw"       | Media is placed at the top-left corner (northwest)              |
-| "n"        | Media is placed on the top and centered horizontally (north)    |
-| "ne"       | Media is placed at the top-right corner (northest)              |
-| "w"        | Media is placed on the left and centered vertically (west)      |
-| "c"        | Media is centered in the target area (center)                   |
-| "e"        | Media is placed on the right and centered vertically (east)     |
-| "sw"       | Media is placed on the bottom-left corner (southwest)           |
-| "s"        | Media is placed on the bottom and centered horizontally (south) |
-| "se"       | Media is placed on the bottom-right corner (southeast)          |
-
-
-
-"sourceX", "sourceY", "sourceWidth", and "sourceHeight" define the
-rectangle of pixels from the media that will be displayed on the
-screen.  The ranges for these values is shown below:
-
-| Key          | Minimum Value | Maximum Value                 | Default Value |
-|--------------|---------------|-------------------------------|---------------|
-| sourceX      | 0             | Media's full width - 1        | 0             |
-| sourceY      | 0             | Media's full height - 1       | 0             |
-| sourceWidth  | 1             | Media's full width - sourceX  | "auto"        |
-| sourceHeight | 1             | Media's full height - sourceY | "auto"        |
-
-If any of these values are specified and outside the range, no image
-is displayed, and the cursor does not move.  "sourceWidth" and
-"sourceHeight" can be "auto", which means use the maximum available
-width/height (given sourceX/sourceY) from the media's inherent
-dimensions.
-
-
-
-Direct Multimedia - Terminal Responses / Error Handling
--------------------------------------------------------
-
-If DMDISPLAY reponses are enabled, then a terminal will respond to the
-DMDISPLAY display with DMRESP.  DMRESP responses must be sent in the
-same sequential order as the DMDISPLAY commands they are responses to:
-the terminal may not re-order responses.
-
-No provision is made for reliable delivery.  On unreliable links
-(example: 3-wire RS232), the DMDISPLAY and DMRESP command/response
-sequence may get out of order.
-
-
-
-The format of DMRESP is:
-
-* Pe - a non-negative integer error code.
-
-* The {args} is a set of key-value pairs (each pair separated by
-  semicolon (';')).
-
-* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z',
-  'a' - 'z').
-
-* A value is any printable ASCII string not containing whitespace,
-  colon, or semicolon ('!' - '9', '<' - '~').
-
-
-
-The Pe error codes are defined as:
-
-| Value | Meaning                            | {args} containts         |
-|-------|------------------------------------|--------------------------|
-| 0     | No error occurred, i.e. success    | nothing                  |
-| 1     | Unsupported "type"          | "type" value that was incorrect |
-| 2     | Invalid value - no media displayed | "key" that was incorrect |
-| 3     | Unsupported key - media displayed  | "key" that unsupported   |
-| 4     | Insufficient memory                | nothing                  |
-| 5     | Other error - no media displayed   | nothing                  |
-| 6     | Other - media displayed            | nothing                  |
-| 7     | Conflicting keys - no media displayed | nothing               |
-| 8     | RESERVED FOR FUTURE USE            | RESERVED FOR FUTURE USE  |
-
-Additional Pe error codes may be returned; any Pe value except 0, 3,
-and 6 must mean that the media was not displayed, and the cursor was
-not moved.
-
-If both "type" and "url" are set, no media is diaplyed, the cursor is
-not moved, and the DMRESP error code is 7.
-
-
-
-Direct Multimedia - Examples
-----------------------------
-
-
-
-Cached Multimedia - Summary
----------------------------
-
-Non-text data (multimedia) can be sent to the terminal for later
-display in a rectangular (single-layer) region of text Cells.
-Multimedia data is transmitted to the terminal using the CMCACHE
-command described below, and displayed on screen using the CMDISPLAY
-command.  A single CMCACHE command can support many CMDISPLAY
-commands.
-
-Upon display, setting a Cell to multimedia is a destructive operation:
-the Cell's original text is lost.  Multimedia pixels will not overlap
-rendered text in the same Cell.  To achieve pixels overlaid on text,
-the layers feature can be used.
-
-Setting any part of a multi-Cell Tile to multimedia also "breaks up"
-the Tile into a range of single Cells.  In other words, multimedia can
-only be carried by a Cell, not a Tile.
-
-The pixels of a multimedia Cell are assigned to the Cell's foreground;
-multimedia Cells have no background.  If a terminal supports the
-layers feature, setting a multimedia Cell's foreground transparency to
-true/enabled causes that Cell to not be displayed at all; setting its
-background transparency to either true/enabled or false/disabled has
-no visible effect.
-
-The pixels of multimedia Cells can come from two sources:
-
-  1. The application can generate pixels and send them to the terminal
-     for display at the current cursor position.
-
-  2. The application can specify a source for the multimedia and the
-     terminal will generate the pixels for display at the current
-     cursor position.
-
-
-
-
-Cached Multimedia - Cache/Memory Management
--------------------------------------------
-
-The terminal manages a cache of multimedia data on behalf of one or
-more applications.  Applications request media be stored in the cache,
-and if successful the terminal provides an identification number that
-applications must use to request display from the cache to the screen.
-
-The amount of memory and retention/eviction strategy for the cache is
-wholly managed by the terminal, with the following restrictions:
-
-* The terminal may not remove items from the cache that have any
-  portion being actively displayed on the primary or alternate
-  screens.
-
-* The terminal must respond to every CMCACHE command with a new unique
-  ID.
-
-The scrollback buffer is permitted, and recommended, to contain only a
-few (or zero) multimedia images.  Terminals should consider retaining
-only the last 2-5 screens' worth of pixel data in the scrollback
-buffer.
-
-
-
-Cached Multimedia - Required Support For Existing Sequences
------------------------------------------------------------
-
-A terminal with cached multimedia feature must support the following
-defined xterm sequences:
-
-| Sequence       | Description                                         |
-|----------------|-----------------------------------------------------|
-| CSI 16 t       | Responds with CSI 6 ; cellHeight ; cellWidth t      |
-| CSI 18 t       | Responds with CSI 8 ; rows ; columns t              |
-
-
-
-Cached Multimedia - New Sequences
----------------------------------
-
-A terminal with cached multimedia feature must support the following new
-sequences:
-
-| Sequence                             | Command   | Description             |
-|--------------------------------------|-----------|-------------------------|
-| CSI ? 3 0 0 0 h             | DECSET 3000 | Enable SCRCHANGE notification  |
-| CSI ? 3 0 0 0 l             | DECRST 3000 | Disable SCRCHANGE notification |
-| OSC 1 3 4 0 ; F i l e = {args} : {data} BEL   | CMCACHE   | Display media at (x, y) |
-| OSC 1 3 4 1 ; Pi ; {args} ST         | CMDISPLAY | Display media at (x, y) |
-| OSC 1 3 4 2 ; Pi ; Pe ; {args} ST | CMCRESP | Terminal response to CMCACHE   |
-| OSC 1 3 4 3 ; Pi ; Pe ; {args} ST | CMDRESP | Terminal response to CMDISPLAY |
-
-
-
-If SCRCHANGE is set/enabled, then the terminal will send the "CSI 6 ;
-cellHeight ; cellWidth t" when the font size has changed, and "CSI 8 ;
-rows ; columns t" when the number of rows/columns on the screen
-changes.
-
-
-
-Cached Multimedia - CMCACHE
----------------------------
-
-For the CMCACHE command:
-
-* The {args} is a set of key-value pairs (each pair separated by
-  semicolon (';')), followed by a colon (':'), followed by a base-64
-  encoded string ({data}).
-
-* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z',
-  'a' - 'z').
-
-* A value is any printable ASCII string not containing whitespace,
-  colon, or semicolon ('!' - '9', '<' - '~').
-
-
-
-The keys for the key-value pairs that must be supported by the
-terminal are listed below:
-
-| Key          | Default Value | Description                                  |
-|--------------|---------------|----------------------------------------------|
-| type         | "image/rgb"   | mime-type describing data field              |
-| url          | ""            | If set, a location containing the media data |
-
-
-
-The "type" value is a mime-type string describing the format of the
-base64-encoded binary data.  The terminal must support at mimunum these
-mime-types:
-
-| Type String   | Description                                                  |
-|---------------|--------------------------------------------------------------|
-| "image/rgb"   | Big-endian-encoded 24-bit red, green, blue values            |
-| "image/rgba"  | Big-endian-encoded 32-bit red, green, blue, alpha values     |
-| "image/png"   | PNG file data as described by (reference to PNG format)      |
-
-A terminal may support additional types.  An application can detect
-terminal support for a format by: sending a CMCACHE command, and
-examining the terminal's CMCRESP sequence for success or error.
-
-
-
-The "url" value is a RFC-XXXX defined Universal Resource Located,
-encoded in RFC-XXXX form as a printable ASCII string not containing:
-whitespace, colon (':'), semicolon (';'), or equals ('=').
-
-A terminal is not required to support any URLs.
-
-
-
-Cached Multimedia - CMDISPLAY
------------------------------
-
-For the CMDISPLAY command:
-
-* Pi - a non-negative integer media ID that was returned by a CMCRESP
-  response to a previous CMCACHE command.
-
-* The {args} is a set of key-value pairs (each pair separated by
-  semicolon (';')), followed by a colon (':'), followed by a base-64
-  encoded string.
-
-* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z',
-  'a' - 'z').
-
-* A value is any printable ASCII string not containing whitespace,
-  colon, or semicolon ('!' - '9', '<' - '~').
-
-* Any alpha-numeric key may be specified.  A key that is not supported
-  by the terminal is ignored without error.
-
-* The multimedia pixels are processed as shown below.
-
-  - The pixel are drawn starting at the upper-left corner of the text
-    cursor position.
-
-  - If scroll is specified as 1 (enabled), then:
-
-    a. The screen is scrolled up if the image overflows into the
-       bottom text row.
-
-    b. The cursor's final position is on the same column as the
-       starting cursor position, and on the row immediately below the
-       image.
-
-  - If scroll is omitted or specified as 0 (disabled), then:
-
-    a. The screen is never scrolled.
-
-    b. Pixels that would be drawn below the visible region on screen
-       are discarded.
-
-    c. The cursor's final position is at the same column and row as
-       the starting cursor position, i.e. the cursor does not move at
-       all.
-
-  - Pixels that would be drawn to the right of the visible region on
-    screen are discarded.
-
-
-
-The keys for the key-value pairs that must be supported by the
-terminal are listed below:
-
-| Key          | Default Value | Description                                  |
-|--------------|---------------|----------------------------------------------|
-| width        | 1             | Number of Cells or pixels wide to display in |
-| height       | 1             | Number of Cells or pixels high to display in |
-| scale        | "none"        | Scale/zoom option, see below                 |
-| align        | "nw"          | Align image to edge option, see below        |
-| sourceX      | 0             | Media source X position to display           |
-| sourceY      | 0             | Media source Y position to display           |
-| sourceWidth  | "auto"        | Media width in pixels to display             |
-| sourceHeight | "auto"        | Media height in pixels to display            |
-| scroll       | 1             | If 1, scroll the display if needed           |
-
-A terminal may support additional keys.  If a key is specified but not
-supported by the terminal, then it is ignored without error.
-
-
-
-The "width" and "height" values can take the following forms:
-
-| Value                         | Meaning                   |
-|-------------------------------|---------------------------|
-| N (a positive integer)        | Number of Cells           |
-| Npx (positive integer + "px") | Number of pixels          |
-| N% (positive integer + "%")   | Percent of screen width or height |
-| "auto"                        | Number of pixels as defined by the multimedia data |
-
-
-
-The "scale" value can take the following values:
-
-| Value      | Meaning                                                       |
-|------------|---------------------------------------------------------------|
-| "none"     | No scaling along either axis.                                 |
-| "scale"    | Stretch image, preserving aspect ratio, to maximum size in the target area without cropping |
-| "stretch"  | Stretch along both axes, distorting aspect ratio, to fill the target area               |
-| "crop"     | Stretch along both axes, preserving aspect ration, to completely fill the target area, cropping pixels that will not fit |
-
-
-
-The "align" value can take the following values:
-
-| Value      | Meaning                                                         |
-|------------|-----------------------------------------------------------------|
-| "nw"       | Media is placed at the top-left corner (northwest)              |
-| "n"        | Media is placed on the top and centered horizontally (north)    |
-| "ne"       | Media is placed at the top-right corner (northest)              |
-| "w"        | Media is placed on the left and centered vertically (west)      |
-| "c"        | Media is centered in the target area (center)                   |
-| "e"        | Media is placed on the right and centered vertically (east)     |
-| "sw"       | Media is placed on the bottom-left corner (southwest)           |
-| "s"        | Media is placed on the bottom and centered horizontally (south) |
-| "se"       | Media is placed on the bottom-right corner (southeast)          |
-
-
-
-"sourceX", "sourceY", "sourceWidth", and "sourceHeight" define the
-rectangle of pixels from the media that will be displayed on the
-screen.  The ranges for these values is shown below:
-
-| Key          | Minimum Value | Maximum Value                 | Default Value |
-|--------------|---------------|-------------------------------|---------------|
-| sourceX      | 0             | Media's full width - 1        | 0             |
-| sourceY      | 0             | Media's full height - 1       | 0             |
-| sourceWidth  | 1             | Media's full width - sourceX  | "auto"        |
-| sourceHeight | 1             | Media's full height - sourceY | "auto"        |
-
-If any of these values are specified and outside the range, no image
-is displayed, and the cursor does not move.  "sourceWidth" and
-"sourceHeight" can be "auto", which means use the maximum available
-width/height (given sourceX/sourceY) from the media's inherent
-dimensions.
-
-
-
-Cached Multimedia - Error Handling
-----------------------------------
-
-A terminal will always respond to the CMCACHE command with CMCRESP,
-and to the CMDISPLAY command with CMDRESP.  Responses must be sent in
-the same sequential order as the CMCACHE/CMDISPLAY commands they are
-responses to: the terminal may not re-order responses.
-
-No provision is made for reliable delivery.  On unreliable links
-(example: 3-wire RS232), the command/response sequence may get out of
-order.
-
-
-
-Cached Multimedia - Error Handling - CMCRESP
---------------------------------------------
-
-The format of CMCRESP is:
-
-* Pi - a non-negative integer media ID.  The terminal will generate a
-  new ID for every image successfully loaded into the cache.  The
-  application must use this ID for CMDISPLAY commands.
-
-* Pe - a non-negative integer error code.
-
-* The {args} is a set of key-value pairs (each pair separated by
-  semicolon (';')).
-
-* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z',
-  'a' - 'z').
-
-* A value is any printable ASCII string not containing whitespace,
-  colon, or semicolon ('!' - '9', '<' - '~').
-
-
-
-The Pe error codes are defined as:
-
-| Value | Meaning                                | {args} containts         |
-|-------|----------------------------------------|--------------------------|
-| 0     | No error occurred, i.e. success        | nothing                  |
-| 1     | Unsupported "type"              | "type" value that was incorrect |
-| 2     | Invalid value - no media stored        | "key" that was incorrect |
-| 3     | Unsupported key - media stored         | "key" that unsupported   |
-| 4     | Insufficient memory - no media stored  | nothing                  |
-| 5     | Other error - no media stored          | nothing                  |
-| 6     | Other - media stored                   | nothing                  |
-| 7     | Conflicting keys - no media stored     | nothing                  |
-| 8     | RESERVED FOR FUTURE USE                | RESERVED FOR FUTURE USE  |
-
-Additional Pe error codes may be returned; any Pe value except 0, 3,
-and 6 must mean that the media was not stored in the cache.
-
-If both "type" and "url" are set, no media is diaplyed, the cursor is
-not moved, and the CMCRESP error code is 7.
-
-
-
-Cached Multimedia - Error Handling - CMDRESP
---------------------------------------------
-
-The format of CMDRESP is:
-
-* Pi - a non-negative integer media ID.
-
-* Pe - a non-negative integer error code.
-
-* The {args} is a set of key-value pairs (each pair separated by
-  semicolon (';')).
-
-* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z',
-  'a' - 'z').
-
-* A value is any printable ASCII string not containing whitespace,
-  colon, or semicolon ('!' - '9', '<' - '~').
-
-
-
-The Pe error codes are defined as:
-
-| Value | Meaning                                | {args} containts         |
-|-------|----------------------------------------|--------------------------|
-| 0     | No error occurred, i.e. success        | nothing                  |
-| 1     | RESERVED FOR FUTURE USE                | RESERVED FOR FUTURE USE  |
-| 2     | Invalid value - no media displayed     | "key" that was incorrect |
-| 3     | Unsupported key - media displayed      | "key" that unsupported   |
-| 4     | Insufficient memory - no media displayed | nothing                |
-| 5     | Other error - no media displayed       | nothing                  |
-| 6     | Other - media displayed                | nothing                  |
-| 7     | RESERVED FOR FUTURE USE                | RESERVED FOR FUTURE USE  |
-| 8     | Media was evicted - no media displayed | nothing                  |
-
-Additional Pe error codes may be returned; any Pe value except 0, 3,
-and 6 must mean that the media was not displayed.
-
-
-
-Cached Multimedia - Examples
-----------------------------
-
-
-
-
-Layers - Summary
-----------------
-
-Layers introduce the concept of a layer "Z" coordinate to the existing
-rows ("Y") by columns ("X") grid.  Put another way, the
-two-dimensional grid of columns-by-rows becomes a three-dimensional
-cube of columns-by-rows-by-layers.  For this document, the column,
-row, and layer coordinates are referred to as X, Y, and Z.  This
-cartesian coordinate system is right-handed, with the Z axis pointing
-"away" from the user "into" the screen.
-
-An application treats the Z coordinate exactly as it does X and Y
-(rows and columns) coordinates:
-
-  * If it attemps to set Z to a value less than 1, then Z is set to 1.
-
-  * If it attempts to set Z to a value greater than the number of
-    layers, then Z is set to the number of layers.
-
-New sequences are provided to set and query Z, Y, X; to set and query
-the screen cube size; and control visibility of Cells in-front-of
-other Cells.
-
-Operations that can act on more than one Cell are defined such to act
-on all layers simultaneously by default; most of these operations can
-also be set to act only on the current layer.
-
-
-
-Layers - Number of Layers
--------------------------
-
-A terminal is required to provide between 1 and a finite number of
-layers.
-
-The number of layers may be different between the primary and
-alternate screens.
-
-An application may request that the terminal allocate additional
-layers.  The terminal is free to honor or ignore such requests as it
-sees fit.
-
-The scrollback buffer is permitted, and recommended, to contain only a
-"flattened" single layer.
-
-
-
-Layers - Terminal State
------------------------
-
-The terminal maintains a complex state at all times.  This state
-includes variables such as cursor position, foreground/background
-color, attributes to apply to the next displayed character, and so on.
-The layers feature adds more variables to the state, and these
-variables are required to be stored with DECSC (ESC 7) and restored
-with DECRC (ESC 8).  The new variables are listed below:
-
-| Mnemonic | Description                 | Default value  |
-|----------|-----------------------------|----------------|
-| Z        | Cursor position Z           | 1              |
-| MSL      | Manipulate single layer     | off / disabled |
-| TFT      | Text foreground transparent | false          |
-| TBT      | Text background transparent | false          |
-
-
-
-Layers - Required Support For Existing Sequences
-------------------------------------------------
-
-A terminal with layers feature must support the standard VT100/VT102
-sequences defined in their respective manuals.
-
-
-
-Layers - New Sequences
-----------------------
-
-A terminal with layer feature must support the following new
-sequences:
-
-| Sequence          | Command     | Description                            |
-|-------------------|-------------|----------------------------------------|
-| CSI ? z ; y ; x H | CUPZ        | Move cursor to (x, y, z)               |
-| CSI 2 2 5 ; 1 ; Pa t | SLA      | Set layer alpha                        |
-| CSI ? 3 0 0 2 h   | DECSET 3002 | Enable Manupulate Single Layer (MSL)   |
-| CSI ? 3 0 0 2 l   | DECRST 3002 | Disable Manupulate Single Layer (MSL)  |
-| CSI ? l ; h ; w t | RSZCUBE     | Resize cube to (layers, height, width) |
-
-Default parameters and ranges are listed below:
-
-| Command | Position / Variable | Default Value | Minumum | Maximum   |
-|---------|---------------------|---------------|---------|-----------|
-| CUPZ    | 1 / z               | 1             | 1       | # layers  |
-| CUPZ    | 2 / y               | 1             | 1       | # rows    |
-| CUPZ    | 3 / x               | 1             | 1       | # columns |
-| SLA     | 1 / alpha           | 255           | 0       | 255       |
-| RSZCUBE | 1 / l               | 1             | 1       | varies    |
-| RSZCUBE | 2 / h               | 24            | 1       | varies    |
-| RSZCUBE | 3 / w               | 80            | 1       | varies    |
-
-The terminal must also support the following new queries:
-
-| Query           | Response              | Description                    |
-|-----------------|-----------------------|--------------------------------|
-| CSI ? 1 0 0 n   | CSI ? z ; y ; x n     | Report cursor Z, Y, X position |
-| CSI ? 1 8 t     | CSI ? 8 ; l ; h ; w t | Report the text area cube layers, height, width |
-
-The terminal must support the following new Set Graphics Rendition
-(SGR) character attributes commands:
-
-| SGR Parameter | Description                                 |
-|---------------|---------------------------------------------|
-| 2 3 0         | Set text foreground color to transparent    |
-| 2 3 9         | Set text foreground color to solid (opaque) |
-| 2 4 0         | Set text background color to transparent    |
-| 2 4 9         | Set text background color to solid (opaque) |
-
-
-
-Layers - Error Handling
------------------------
-
-No additional error reporting is provided for layer feature.
-
-
-
-Layers - Rendering
-------------------
-
-A terminal with layer feature will display its Cells such that the
-screen will appear as if it was rendered in the manner of the
-pseudo-code below:
-
-```
-for each layer Z, in descending order from maxZ to minZ:
-
-  for each row Y, in ascending order from minY to maxY:
-
-    for each column X, in ascending order from minX to maxX:
-
-      if tile at (X, Y, Z) background color is solid:
-        draw rectangle of background color with layer alpha
-
-      if tile at (X, Y, Z) foreground color is solid:
-        if tile at (X, Y, Z) is glyph:
-          draw glyph with foreground color with layer alpha
-        else
-          draw pixel data of tile as red/green/blue/alpha pixels with
-             layer alpha
-
-      advance X by tile width
-    next column
-
-    advance Y by 1
-  next row
-
-  decrease Z by 1
-next layer
-```
-
-A terminal is free to optimize its rendering as it sees fit, so long
-as the final screen output looks equivalent to the above method.
-
-
-
-Layers - Integration With Existing Sequences
---------------------------------------------
-
-Sequences that insert characters/lines, delete characters/lines, or
-modify larger regions are changed to act upon multiple layers as
-defined below.  By default, MSL (Manipulate Single Layer) is
-off/unset, and Z is 1, so if the application never changes MSL or Z
-then these sequences will produce the same visible output as a
-terminal without layer support.
-
-A terminal is not required to support all of these sequences; however,
-for those sequences it does support, if it supports the layers feature
-then the sequences must behave as shown below:
-
-| Sequence   | Command     | Additional behavior                      |
-|------------|-------------|------------------------------------------|
-| BS  (0x08) | Backspace   | Only current layer affected if MSL=on    |
-| DEL (0x7F) | Delete      | Only current layer affected if MSL=on    |
-| IND (0x84) | Index       | Only current layer affected if MSL=on    |
-| RI  (0x8D  | Reverse Index | Only current layer affected if MSL=on  |
-| ESC # 3    | DECDHL      | Cells on all layers always affected      |
-| ESC # 4    | DECDHL      | Cells on all layers always affected      |
-| ESC # 5    | DECSWL      | Cells on all layers always affected      |
-| ESC # 6    | DECDWL      | Cells on all layers always affected      |
-| ESC # 8    | DECALN      | All layers > 1 cleared; Z, MSL, TFT, TBT reset to default |
-| ESC 7      | DECSC       | Also store Z, MSL, TFT, TBT              |
-| ESC 8      | DECRC       | Also restore Z, MSL, TFT, TBT            |
-| ESC c      | RIS         | All layers > 1 cleared; Z, MSL, TFT, TBT reset to default |
-| CSI @      | ICH         | Only current layer affected if MSL=on    |
-| CSI J      | ED          | Only current layer affected if MSL=on    |
-| CSI K      | EL          | Only current layer affected if MSL=on    |
-| CSI ? K    | DECSEL      | Only current layer affected if MSL=on    |
-| CSI L      | IL          | Only current layer affected if MSL=on    |
-| CSI M      | DL          | Only current layer affected if MSL=on    |
-| CSI X      | ECH         | Only current layer affected if MSL=on    |
-| CSI M      | DL          | Only current layer affected if MSL=on    |
-| CSI P      | DCH         | Only current layer affected if MSL=on    |
-| CSI R      | DECSTBM     | Cells on all layers always affected      |
-| CSI $ t    | DECARA      | Only current layer affected if MSL=on    |
-| CSI $ v    | DECCRA      | Only current layer affected if MSL=on    |
-| CSI x      | DECSACE     | Cells on all layers always affected      |
-| CSI $ x    | DECFRA      | Only current layer affected if MSL=on    |
-| CSI $ z    | DECERA      | Only current layer affected if MSL=on    |
-
-(( TODO: add many more to the above table... ))
-
-The VT52 sub-mode commands:
-
-| Sequence   | Command     | Additional behavior                      |
-|------------|-------------|------------------------------------------|
-| ESC J      | ED          | Only current layer affected if MSL=on    |
-| ESC K      | EL          | Only current layer affected if MSL=on    |
-
-
-
-Layers - Use With Multiplexers
-------------------------------
-
-Layers are inteded to provide a means for multiplexers to pass on the
-job of multimedia support to the "outer" or host terminal.  The
-proposed mechanics of that is outlined in the pseudo-code below:
-
-```
-for each inner terminal in descending order from maxZ to minZ:
-
-  emit CUPZ(inner terminal Z, inner terminal Y, inner terminal X)
-
-  draw inner terminal text with standard VT100/VT102/xterm sequences
-
-  for each multimedia sequence emitted by the inner terminal:
-    emit CUP(inner terminal Y, inner terminal X)
-    emit multimedia sequences to outer terminal
-  next multimedia sequence
-
-  decrease Z by 1
-next inner terminal
-```
-
-The method above may not be effective for complex multi-terminal
-screen layouts, but is hoped to work well for many simple cases.
-
-
-
-Layers - Examples
------------------
-
-
-
-
-References
-----------
-
-* xterm control sequences:
-
-
-* ECMA-48:
diff --git a/docs/images2.md b/docs/images2.md
deleted file mode 100644 (file)
index 26268cf..0000000
+++ /dev/null
@@ -1,524 +0,0 @@
-Terminal Emulator Images Standard - Proposed Design - Simplified
-================================================================
-
-Version: 1
-
-
-
-Purpose
--------
-
-See the [original proposal](images.md) for purpose, design goals, and
-definitions.
-
-This document is an updated proposal to address feedback on the first
-proposal, which included: "overengineered", "hopelessly
-overengineered", and "unnecessarily complex."  I perceive this
-feedback as a positive: it is far easier to imagine a feature and
-remove it, than to fail to picture it and need to shoehorn it in
-later.
-
-The original proposal was a superset of every image format referenced,
-and generalized beyond to multimedia.  This proposal is sharply
-reduced from that to: "put this pixel rectangle from the image, into
-that cell-based rectangle with specific scaling policy".  It is mostly
-a subset of the iTerm2 protocol, with:
-
-* Specifications for what happens to the cursor.
-
-* More precise definitions of the "preserveAspectRatio" equivalent
-  options.
-
-* Explicit restriction to a Cell-based target region.
-
-* Definition that pixels not covered by image are set to the current
-  background color.
-
-
-
-Tradeoffs
----------
-
-Simplifying the original proposal will significantly reduce
-complexity, but also eliminates features.  The major tradeoffs offered
-in this revised proposal are:
-
-1. Elimination of the layers feature, and with it the ability to place
-   images behind text.  In this proposal, a Cell on the screen will
-   show either a (part of a) visible image, or a (part of a) text
-   glyph, but never both.
-
-2. Elimination of the "url" option, and with it the ability for an
-   application to specify a filename or other method for the terminal
-   to find the file data on the local machine.  Image data must always
-   be passed inline with the sequences.
-
-3. Elimination of response codes, and with it:
-
-   - The ability for multiplexers to blindly pass on the sequences to
-     their host terminal (because unique IDs are not generated by the
-     terminal).
-
-   - The ability for applications to reliably detect success or
-     failure of image display operations.
-
-4. Elimination of pixel-oriented image placement operations, and with
-   it the ability of applications to pass on image calculations to the
-   terminal.  An application which requires pixel-perfect rendering
-   must generate the pixels it needs, aligned such to be displayed at
-   the top-left corner of the text Cell rectangle.
-
-
-
-Summary
--------
-
-This revised document proposes two independent new features:
-
-1. A method to transfer image data for immediate display within the
-   screen Cell grid ("Direct Images").
-
-2. A method to transfer image data to a terminal-managed cache, and
-   later display that data within the screen Cell grid ("Cached
-   Images").
-
-The only difference between the first and second feature is the
-presence of an ID key.  Direct images do not use an ID key, while
-cached images use a store operation with ID key followed by one or
-more display operations with ID key.
-
-Images are applied to text Cells, and once set handled the same way
-text Cells are handled: erasing a line erases the image Cells on that
-line, inserting a character will shift image Cells on that row over,
-scrolling will shift the image up, and so on.  Therefore, terminals
-will need to be prepared for the scenario that every Cell on the
-display is a separate image, with a separate display scaling option
-that will need to be re-applied automatically if font metrics change.
-
-
-
-All Features - Detection
-------------------------
-
-Applications can detect support for these features using Primary
-Device Attributes (DA) and DECID (ESC Z, or 0x9A).
-
-Terminals that support this standard will repond with additional
-parameter(s): "224" for direct images and "225" for cached images.  A
-recap of the parameters xterm supports is listed below, with these new
-feature responses included:
-
-| VT220 (and higher) Response | Description                                |
-|-----------------------------|--------------------------------------------|
-| 1                           | 132-columns                                |
-| 2                           | Printer                                    |
-| 3                           | ReGIS graphics                             |
-| 4                           | Sixel graphics                             |
-| 6                           | Selective erase                            |
-| 8                           | User-defined keys                          |
-| 9                           | National Replacement Character sets        |
-| 1 5                         | Technical characters                       |
-| 1 6                         | Locator port                               |
-| 1 7                         | Terminal state interrogation               |
-| 1 8                         | User windows                               |
-| 2 1                         | Horizontal scrolling                       |
-| 2 2                         | ANSI color, e.g., VT525                    |
-| 2 8                         | Rectangular editing                        |
-| 2 9                         | ANSI text locator (i.e., DEC Locator mode) |
-| 2 2 4                       | Direct Images Version 1                    |
-| 2 2 5                       | Cached Images Version 1                    |
-
-
-
-Direct Images - Summary
------------------------
-
-Non-text data (images) can be sent to the terminal for immediate
-display in a rectangular region of text Cells.  Image data is
-transmitted to the terminal using a wire format described later in
-this document.
-
-Setting a Cell to image is a destructive operation: the Cell's
-original text is lost.  Similarly, setting a Cell (or multiple Cells
-for fullwidth glyphs or grapheme clusters) to text is a destructive
-operation: the image in the Cell(s) is lost.
-
-Setting any part of a multi-Cell Tile to image also "breaks up" the
-Tile into a range of single Cells.  In other words, image data can
-only be carried by a Cell, not a Tile.
-
-
-
-Direct Images - New Sequences
------------------------------
-
-A terminal with direct images feature must support the following new
-sequences:
-
-| Sequence                             | Description             |
-|--------------------------------------|-------------------------|
-| OSC 1 3 3 8 ; F i l e = {args} : {data} BEL | Display image at (x, y) |
-| OSC 1 3 3 8 ; F i l e = {args} : {data} ST  | Display image at (x, y) |
-
-
-
-For the OSC 1 3 3 8 sequence:
-
-* The {args} is a set of key-value pairs (each pair separated by
-  semicolon (';')), followed by a colon (':'), followed by a base-64
-  encoded string ({data}).
-
-* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z',
-  'a' - 'z').
-
-* A value is any printable ASCII string not containing whitespace,
-  colon, or semicolon ('!' - '9', '<' - '~').
-
-* Any alpha-numeric key may be specified.  A key that is not supported
-  by the terminal is ignored without error.
-
-* The image is processed as shown below:
-
-  - The pixels are drawn starting at the upper-left corner of the text
-    cursor position.
-
-  - All pixels in the target Cell rectangle that are not covered by
-    the image itself are set the current background color (like
-    sixel raster attributes).
-
-  - If scroll is specified as 1 (enabled), then:
-
-    a. The screen is scrolled up if the image overflows into the
-       bottom text row.
-
-    b. The cursor's final position is on the same column as the
-       starting cursor position, and on the row immediately below the
-       image.
-
-  - If scroll is omitted or specified as 0 (disabled), then:
-
-    a. The screen is never scrolled.
-
-    b. Pixels that would be drawn below the visible region on screen
-       are discarded.
-
-    c. The cursor's final position is at the same column and row as
-       the starting cursor position, i.e. the cursor does not move at
-       all.
-
-  - Pixels that would be drawn to the right of the visible region on
-    screen are discarded.
-
-  - If scale is "none", then pixels that would be drawn outside the
-    target Cell rectangle are discarded.
-
-
-
-The keys for the key-value pairs that must be supported by the
-terminal are listed below:
-
-| Key          | Default Value | Description                           |
-|--------------|---------------|---------------------------------------|
-| type         | "image/rgb"   | mime-type describing data field       |
-| width        | 1             | Number of Cell columns to display in  |
-| height       | 1             | Number of Cells rows to display in    |
-| scale        | "none"        | Scale/zoom option, see below          |
-| sourceX      | 0             | Media source X position to display    |
-| sourceY      | 0             | Media source Y position to display    |
-| sourceWidth  | "auto"        | Media width in pixels to display      |
-| sourceHeight | "auto"        | Media height in pixels to display     |
-| scroll       | 0             | If 0, scroll the display if needed    |
-
-A terminal may support additional keys.  If a key is specified but not
-supported by the terminal, then it is ignored without error.
-
-
-
-The "type" value is a mime-type string describing the format of the
-base64-encoded binary data.  The terminal must support at minimum these
-mime-types:
-
-| Type String   | Description                                                  |
-|---------------|--------------------------------------------------------------|
-| "image/rgb"   | Big-endian-encoded 24-bit red, green, blue values            |
-| "image/rgba"  | Big-endian-encoded 32-bit red, green, blue, alpha values     |
-| "image/png"   | PNG file data as described by (reference to PNG format)      |
-
-A terminal may support additional types.  An application can detect
-terminal support for a format by:
-
-  1. Attempt to draw image, with "scroll" set to 1.
-
-  2. Check cursor position DSR 6.
-
-  3. If cursor has moved, then the terminal supports this image type.
-
-
-
-The "width" and "height" values are positive integers describing the
-number of Cells the image will be placed in.
-
-
-
-The "scale" value can take the following values:
-
-| Value      | Meaning                                                       |
-|------------|---------------------------------------------------------------|
-| "none"     | No scaling along either axis.                                 |
-| "scale"    | Stretch image, preserving aspect ratio, to maximum size in the target area without cropping |
-| "stretch"  | Stretch along both axes, distorting aspect ratio, to fill the target area               |
-| "crop"     | Stretch along both axes, preserving aspect ration, to completely fill the target area, cropping pixels that will not fit |
-
-
-
-"sourceX", "sourceY", "sourceWidth", and "sourceHeight" define the
-rectangle of pixels from the media that will be displayed on the
-screen.  The ranges for these values is shown below:
-
-| Key          | Minimum Value | Maximum Value                 | Default Value |
-|--------------|---------------|-------------------------------|---------------|
-| sourceX      | 0             | Media's full width - 1        | 0             |
-| sourceY      | 0             | Media's full height - 1       | 0             |
-| sourceWidth  | 1             | Media's full width - sourceX  | "auto"        |
-| sourceHeight | 1             | Media's full height - sourceY | "auto"        |
-
-If any of these values are specified and outside the range, no image
-is displayed, and the cursor does not move.  "sourceWidth" and
-"sourceHeight" can be "auto", which means use the maximum available
-width/height (given sourceX/sourceY) from the media's inherent
-dimensions.
-
-
-
-Cached Images - Summary
------------------------
-
-Non-text data (image) can be sent to the terminal for later display in
-a rectangular region of text Cells.  Image data is transmitted to the
-terminal using the CSTORE command described below, and displayed on
-screen using the CDISPLAY command.  A single CSTORE command can
-support many CDISPLAY commands.
-
-Upon display, setting a Cell to image is a destructive operation: the
-Cell's original text is lost.  Similarly, setting a Cell (or multiple
-Cells for fullwidth glyphs or grapheme clusters) to text is a
-destructive operation: the image in the Cell(s) is lost.
-
-Setting any part of a multi-Cell Tile to image also "breaks up" the
-Tile into a range of single Cells.  In other words, image data can
-only be carried by a Cell, not a Tile.
-
-
-
-Cached Images - Cache/Memory Management
----------------------------------------
-
-The terminal manages a cache of multimedia data on behalf of the
-application.  The application requests media be stored in the cache
-and provides an ID.  This ID is later used to request display on the
-screen.
-
-The amount of memory and retention/eviction strategy for the cache is
-wholly managed by the terminal, with the following restrictions:
-
-* The terminal may not remove items from the cache that have any
-  portion being actively displayed on the primary or alternate
-  screens.
-
-The scrollback buffer is permitted, and recommended, to contain only a
-few (or zero) multimedia images.  Terminals should consider retaining
-only the last 2-5 screens' worth of pixel data in the scrollback
-buffer.
-
-Applications have no control over when images are removed from the
-cache, and no provision is made to generate/ensure unique IDs.
-
-A terminal multiplexer that passes all CSTORE/CDISPLAY commands to the
-host terminal will need to parse the CSTORE and CDISPLAY sequences for
-the "id" field and rewrite it to be unique for all of its inner
-terminals.
-
-
-
-Cached Images - New Sequences
------------------------------
-
-A terminal with cached images feature must support the following new
-sequences:
-
-| Sequence                             | Command   | Description              |
-|--------------------------------------|-----------|--------------------------|
-| OSC 1 3 4 0 ; F i l e = {args} : {data} BEL | CSTORE | Store image in cache |
-| OSC 1 3 4 0 ; F i l e = {args} : {data} ST  | CSTORE | Store image in cache |
-| OSC 1 3 4 1 ; Pi ; {args} BEL        | CDISPLAY  | Display image at (x, y)  |
-| OSC 1 3 4 1 ; Pi ; {args} ST         | CDISPLAY  | Display image at (x, y)  |
-
-
-
-Cached Images - CSTORE
-----------------------
-
-For the CSTORE command:
-
-* The {args} is a set of key-value pairs (each pair separated by
-  semicolon (';')), followed by a colon (':'), followed by a base-64
-  encoded string ({data}).
-
-* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z',
-  'a' - 'z').
-
-* A value is any printable ASCII string not containing whitespace,
-  colon, or semicolon ('!' - '9', '<' - '~').
-
-
-
-The keys for the key-value pairs that must be supported by the
-terminal are listed below:
-
-| Key          | Default Value | Description                                  |
-|--------------|---------------|----------------------------------------------|
-| id           | 0             | ID to refer to the image                     |
-| type         | "image/rgb"   | mime-type describing data field              |
-
-
-
-The "id" value is a non-negative integer between 0 and 999999.
-
-
-
-The "type" value is a mime-type string describing the format of the
-base64-encoded binary data.  The terminal must support at mimunum these
-mime-types:
-
-| Type String   | Description                                                  |
-|---------------|--------------------------------------------------------------|
-| "image/rgb"   | Big-endian-encoded 24-bit red, green, blue values            |
-| "image/rgba"  | Big-endian-encoded 32-bit red, green, blue, alpha values     |
-| "image/png"   | PNG file data as described by (reference to PNG format)      |
-
-A terminal may support additional types.  An application can detect
-terminal support for a format by:
-
-  1. Store image in cache.
-
-  2. Attempt to draw image, with "scroll" set to 1.
-
-  3. Check cursor position DSR 6.
-
-  4. If cursor has moved, then the terminal supports this image type.
-
-
-
-Cached Images - CDISPLAY
-------------------------
-
-For the CDISPLAY command:
-
-* Pi - a non-negative integer ID that was used in a previous CSTORE
-  command.
-
-* The {args} is a set of key-value pairs (each pair separated by
-  semicolon (';')), followed by a colon (':'), followed by a base-64
-  encoded string.
-
-* A key can be any alpha-numeric ASCII string ('0' - '9', 'A' - 'Z',
-  'a' - 'z').
-
-* A value is any printable ASCII string not containing whitespace,
-  colon, or semicolon ('!' - '9', '<' - '~').
-
-* Any alpha-numeric key may be specified.  A key that is not supported
-  by the terminal is ignored without error.
-
-* The image pixels are processed as shown below.
-
-  - The pixel are drawn starting at the upper-left corner of the text
-    cursor position.
-
-  - If scroll is specified as 1 (enabled), then:
-
-    a. The screen is scrolled up if the image overflows into the
-       bottom text row.
-
-    b. The cursor's final position is on the same column as the
-       starting cursor position, and on the row immediately below the
-       image.
-
-  - If scroll is omitted or specified as 0 (disabled), then:
-
-    a. The screen is never scrolled.
-
-    b. Pixels that would be drawn below the visible region on screen
-       are discarded.
-
-    c. The cursor's final position is at the same column and row as
-       the starting cursor position, i.e. the cursor does not move at
-       all.
-
-  - Pixels that would be drawn to the right of the visible region on
-    screen are discarded.
-
-
-
-The keys for the key-value pairs that must be supported by the
-terminal are listed below:
-
-| Key          | Default Value | Description                           |
-|--------------|---------------|---------------------------------------|
-| id           | 0             | ID to refer to the image              |
-| width        | 1             | Number of Cell columns to display in  |
-| height       | 1             | Number of Cells rows to display in    |
-| scale        | "none"        | Scale/zoom option, see below          |
-| sourceX      | 0             | Media source X position to display    |
-| sourceY      | 0             | Media source Y position to display    |
-| sourceWidth  | "auto"        | Media width in pixels to display      |
-| sourceHeight | "auto"        | Media height in pixels to display     |
-| scroll       | 0             | If 1, scroll the display if needed    |
-
-A terminal may support additional keys.  If a key is specified but not
-supported by the terminal, then it is ignored without error.
-
-
-
-The "width" and "height" values are positive integers describing the
-number of Cells the image will be placed in.
-
-
-
-The "scale" value can take the following values:
-
-| Value      | Meaning                                                       |
-|------------|---------------------------------------------------------------|
-| "none"     | No scaling along either axis.                                 |
-| "scale"    | Stretch image, preserving aspect ratio, to maximum size in the target area without cropping |
-| "stretch"  | Stretch along both axes, distorting aspect ratio, to fill the target area               |
-| "crop"     | Stretch along both axes, preserving aspect ration, to completely fill the target area, cropping pixels that will not fit |
-
-
-
-"sourceX", "sourceY", "sourceWidth", and "sourceHeight" define the
-rectangle of pixels from the media that will be displayed on the
-screen.  The ranges for these values is shown below:
-
-| Key          | Minimum Value | Maximum Value                 | Default Value |
-|--------------|---------------|-------------------------------|---------------|
-| sourceX      | 0             | Media's full width - 1        | 0             |
-| sourceY      | 0             | Media's full height - 1       | 0             |
-| sourceWidth  | 1             | Media's full width - sourceX  | "auto"        |
-| sourceHeight | 1             | Media's full height - sourceY | "auto"        |
-
-If any of these values are specified and outside the range, no image
-is displayed, and the cursor does not move.  "sourceWidth" and
-"sourceHeight" can be "auto", which means use the maximum available
-width/height (given sourceX/sourceY) from the media's inherent
-dimensions.
-
-
-
-Miscellaneous Items
--------------------
-
-"image/rgb" and "image/rgba" also need width/height fields.  Propose
-to specify them as 16-bit unsigned ints, followed by 24-bit or 32-bit
-data.  If data is short, then the rest of the image is assumed to be
-current background color (like sixel raster attributes).
similarity index 87%
rename from src/jexer/event/TMouseEvent.java
rename to event/TMouseEvent.java
index 496d8bc06422baa3014a95a98ae953f60cd4325e..e52989814005bec2d2d303372519276ed4e4410f 100644 (file)
@@ -118,6 +118,21 @@ public class TMouseEvent extends TInputEvent {
      */
     private boolean mouseWheelDown;
 
+    /**
+     * Keyboard modifier ALT.
+     */
+    private boolean alt;
+
+    /**
+     * Keyboard modifier CTRL.
+     */
+    private boolean ctrl;
+
+    /**
+     * Keyboard modifier SHIFT.
+     */
+    private boolean shift;
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -135,11 +150,15 @@ public class TMouseEvent extends TInputEvent {
      * @param mouse3 if true, middle button is down
      * @param mouseWheelUp if true, mouse wheel (button 4) is down
      * @param mouseWheelDown if true, mouse wheel (button 5) is down
+     * @param alt if true, ALT was pressed with this mouse event
+     * @param ctrl if true, CTRL was pressed with this mouse event
+     * @param shift if true, SHIFT was pressed with this mouse event
      */
     public TMouseEvent(final Type type, final int x, final int y,
         final int absoluteX, final int absoluteY,
         final boolean mouse1, final boolean mouse2, final boolean mouse3,
-        final boolean mouseWheelUp, final boolean mouseWheelDown) {
+        final boolean mouseWheelUp, final boolean mouseWheelDown,
+        final boolean alt, final boolean ctrl, final boolean shift) {
 
         this.type               = type;
         this.x                  = x;
@@ -151,6 +170,9 @@ public class TMouseEvent extends TInputEvent {
         this.mouse3             = mouse3;
         this.mouseWheelUp       = mouseWheelUp;
         this.mouseWheelDown     = mouseWheelDown;
+        this.alt                = alt;
+        this.ctrl               = ctrl;
+        this.shift              = shift;
     }
 
     // ------------------------------------------------------------------------
@@ -289,6 +311,33 @@ public class TMouseEvent extends TInputEvent {
         return mouseWheelDown;
     }
 
+    /**
+     * Getter for ALT.
+     *
+     * @return alt value
+     */
+    public boolean isAlt() {
+        return alt;
+    }
+
+    /**
+     * Getter for CTRL.
+     *
+     * @return ctrl value
+     */
+    public boolean isCtrl() {
+        return ctrl;
+    }
+
+    /**
+     * Getter for SHIFT.
+     *
+     * @return shift value
+     */
+    public boolean isShift() {
+        return shift;
+    }
+
     /**
      * Create a duplicate instance.
      *
@@ -296,7 +345,9 @@ public class TMouseEvent extends TInputEvent {
      */
     public TMouseEvent dup() {
         TMouseEvent mouse = new TMouseEvent(type, x, y, absoluteX, absoluteY,
-            mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown);
+            mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown,
+            alt, ctrl, shift);
+
         return mouse;
     }
 
@@ -307,7 +358,7 @@ public class TMouseEvent extends TInputEvent {
      */
     @Override
     public String toString() {
-        return String.format("Mouse: %s x %d y %d absoluteX %d absoluteY %d 1 %s 2 %s 3 %s DOWN %s UP %s",
+        return String.format("Mouse: %s x %d y %d absoluteX %d absoluteY %d 1 %s 2 %s 3 %s DOWN %s UP %s ALT %s CTRL %s SHIFT %s",
             type,
             x, y,
             absoluteX, absoluteY,
@@ -315,7 +366,8 @@ public class TMouseEvent extends TInputEvent {
             mouse2,
             mouse3,
             mouseWheelUp,
-            mouseWheelDown);
+            mouseWheelDown,
+            alt, ctrl, shift);
     }
 
 }
diff --git a/examples/HelloWorld.java b/examples/HelloWorld.java
deleted file mode 100644 (file)
index 1246959..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-import jexer.TApplication;
-
-public class HelloWorld {
-
-    public static void main(String [] args) throws Exception {
-        TApplication app = new TApplication(TApplication.BackendType.XTERM);
-        app.addToolMenu();
-        app.addFileMenu();
-        app.addWindowMenu();
-        app.run();
-    }
-}
diff --git a/examples/JexerImageViewer.java b/examples/JexerImageViewer.java
deleted file mode 100644 (file)
index 4839c23..0000000
+++ /dev/null
@@ -1,312 +0,0 @@
-import java.awt.image.BufferedImage;
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import javax.imageio.ImageIO;
-
-import jexer.TAction;
-import jexer.TApplication;
-import jexer.TDesktop;
-import jexer.TDirectoryList;
-import jexer.TImage;
-import jexer.backend.SwingTerminal;
-import jexer.bits.CellAttributes;
-import jexer.bits.GraphicsChars;
-import jexer.event.TKeypressEvent;
-import jexer.event.TResizeEvent;
-import jexer.menu.TMenu;
-import jexer.ttree.TDirectoryTreeItem;
-import jexer.ttree.TTreeItem;
-import jexer.ttree.TTreeViewWidget;
-import static jexer.TKeypress.*;
-
-/**
- * Implements a simple image thumbnail file viewer.  Much of this code was
- * stripped down from TFileOpenBox.
- */
-public class JexerImageViewer extends TApplication {
-
-    /**
-     * Main entry point.
-     */
-    public static void main(String [] args) throws Exception {
-        JexerImageViewer app = new JexerImageViewer();
-        (new Thread(app)).start();
-    }
-
-    /**
-     * Public constructor chooses the ECMA-48 / Xterm backend.
-     */
-    public JexerImageViewer() throws Exception {
-        super(BackendType.XTERM);
-
-        // The stock tool menu has items for redrawing the screen, opening
-        // images, and (when using the Swing backend) setting the font.
-        addToolMenu();
-
-        // We will have one menu containing a mix of new and stock commands
-        TMenu fileMenu = addMenu("&File");
-
-        // Stock commands: a new shell, exit program.
-        fileMenu.addDefaultItem(TMenu.MID_SHELL);
-        fileMenu.addSeparator();
-        fileMenu.addDefaultItem(TMenu.MID_EXIT);
-
-        // Filter the files list to support image suffixes only.
-        List<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);
-    }
-
-}
diff --git a/examples/JexerTilingWindowManager.java b/examples/JexerTilingWindowManager.java
deleted file mode 100644 (file)
index 5b5740a..0000000
+++ /dev/null
@@ -1,223 +0,0 @@
-import jexer.TApplication;
-import jexer.TTerminalWindow;
-import jexer.TWindow;
-import jexer.event.TKeypressEvent;
-import jexer.event.TMenuEvent;
-import jexer.event.TMouseEvent;
-import jexer.event.TResizeEvent;
-import jexer.menu.TMenu;
-
-/**
- * Implements a simple tiling window manager.  A root non-moveable
- * non-resizable terminal window is created first, which can be split
- * horizontally or vertically.  Each new window retains a reference to its
- * "parent", and upon closing resizes that parent back to its original size.
- *
- * This example shows what can be done with minimal changes to stock Jexer
- * widgets. You will quickly see that closing a "parent" tile does not cause
- * the "child" tile to resize.  You could make a real subclass of
- * TTerminalWindow that has extra fields and/or communicates more with
- * JexerTilingWindowManager to get full coverage of tile creation,
- * destruction, placement, movement, and so on.
- */
-public class JexerTilingWindowManager extends TApplication {
-
-    /**
-     * Menu item: split the terminal vertically.
-     */
-    private static final int MENU_SPLIT_VERTICAL = 2000;
-
-    /**
-     * Menu item: split the terminal horizontally.
-     */
-    private static final int MENU_SPLIT_HORIZONTAL = 2001;
-
-    /**
-     * Main entry point.
-     */
-    public static void main(String [] args) throws Exception {
-        // For this application, we must use ptypipe so that the tile shells
-        // can be aware of their size.
-        System.setProperty("jexer.TTerminal.ptypipe", "true");
-
-        JexerTilingWindowManager jtwm = new JexerTilingWindowManager();
-        (new Thread(jtwm)).start();
-    }
-
-    /**
-     * Public constructor chooses the ECMA-48 / Xterm backend.
-     */
-    public JexerTilingWindowManager() throws Exception {
-        super(BackendType.XTERM);
-
-        // The stock tool menu has items for redrawing the screen, opening
-        // images, and (when using the Swing backend) setting the font.
-        addToolMenu();
-
-        // We will have one menu containing a mix of new and stock commands
-        TMenu tileMenu = addMenu("&Tile");
-
-        // New commands for this example: split vertical and horizontal.
-        tileMenu.addItem(MENU_SPLIT_VERTICAL, "&Vertical Split");
-        tileMenu.addItem(MENU_SPLIT_HORIZONTAL, "&Horizontal Split");
-
-        // Stock commands: a new shell with resizable window, previous, next,
-        // close, and exit program.
-        tileMenu.addItem(TMenu.MID_SHELL, "&Floating");
-        tileMenu.addSeparator();
-        tileMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
-        tileMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
-        tileMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
-        tileMenu.addSeparator();
-        tileMenu.addDefaultItem(TMenu.MID_EXIT);
-
-        // Spin up the root tile
-        TTerminalWindow rootTile = makeTile(0, 0, getScreen().getWidth(),
-            getDesktopBottom() - 1, null);
-
-        // Let's add some bling!  Enable focus-follows-mouse.
-        setFocusFollowsMouse(true);
-    }
-
-    /**
-     * Process menu events.
-     */
-    @Override
-    protected boolean onMenu(TMenuEvent event) {
-        if (event.getId() == MENU_SPLIT_VERTICAL) {
-            splitVertical();
-            return true;
-        }
-        if (event.getId() == MENU_SPLIT_HORIZONTAL) {
-            splitHorizontal();
-            return true;
-        }
-
-        return super.onMenu(event);
-    }
-
-    /**
-     * Perform the vertical split.
-     */
-    private void splitVertical() {
-        TWindow window = getActiveWindow();
-        if (!(window instanceof TTerminalWindow)) {
-            return;
-        }
-
-        TTerminalWindow tile = (TTerminalWindow) window;
-        // Give the extra column to the new tile.
-        int newWidth = (tile.getWidth() + 1) / 2;
-        int newY = tile.getY() - 1;
-        int newX = tile.getX() + tile.getWidth() - newWidth;
-        makeTile(newX, newY, newWidth, tile.getHeight(), tile);
-        tile.setWidth(tile.getWidth() - newWidth);
-        tile.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
-                tile.getWidth(), tile.getHeight()));
-    }
-
-    /**
-     * Perform the horizontal split.
-     */
-    private void splitHorizontal() {
-        TWindow window = getActiveWindow();
-        if (!(window instanceof TTerminalWindow)) {
-            return;
-        }
-
-        TTerminalWindow tile = (TTerminalWindow) window;
-        // Give the extra row to the new tile.
-        int newHeight = (tile.getHeight() + 1) / 2;
-        int newY = tile.getY() - 1 + tile.getHeight() - newHeight;
-        int newX = tile.getX();
-        makeTile(newX, newY, tile.getWidth(), newHeight, tile);
-        tile.setHeight(tile.getHeight() - newHeight);
-        tile.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
-                tile.getWidth(), tile.getHeight()));
-    }
-
-    /**
-     * Create a non-resizable non-movable terminal window.
-     *
-     * @param x the column number to place the top-left corner at.  0 is the
-     * left-most column.
-     * @param y the row number to place the top-left corner at.  0 is the
-     * top-most column.
-     * @param width the width of the window
-     * @param height the height of the window
-     * @param otherTile the other tile to resize when this window closes
-     */
-    private TTerminalWindow makeTile(int x, int y, int width, int height,
-        final TTerminalWindow otherTile) {
-
-        // We pass flags to disable the zoom (maximize) button, disable
-        // "smart" window placement, and set the specific location.
-        TTerminalWindow tile = new TTerminalWindow(this, x, y,
-            TWindow.NOZOOMBOX | TWindow.ABSOLUTEXY,
-            new String[] { "/bin/bash", "--login" }, true) {
-
-            /**
-             * When this terminal closes, if otherTile is defined then resize
-             * it to overcover me.
-             */
-            @Override
-            public void onClose() {
-                super.onClose();
-
-                if (otherTile != null) {
-                    if (otherTile.getX() != getX()) {
-                        // Undo the vertical split
-                        otherTile.setX(Math.min(otherTile.getX(), getX()));
-                        otherTile.setWidth(otherTile.getWidth() + getWidth());
-                    }
-                    if (otherTile.getY() != getY()) {
-                        otherTile.setY(Math.min(otherTile.getY(), getY()));
-                        otherTile.setHeight(otherTile.getHeight() + getHeight());
-                    }
-                    otherTile.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
-                            otherTile.getWidth(), otherTile.getHeight()));
-                }
-            }
-
-            /**
-             * Prevent the user from resizing or moving this window.
-             */
-            @Override
-            public void onMouseDown(final TMouseEvent mouse) {
-                super.onMouseDown(mouse);
-                stopMovements();
-            }
-
-            /**
-             * Prevent the user from resizing or moving this window.
-             */
-            @Override
-            public void onKeypress(final TKeypressEvent keypress) {
-                super.onKeypress(keypress);
-                stopMovements();
-            }
-
-            /**
-             * Permit the user to use all of the menu items.
-             */
-            @Override
-            public void onIdle() {
-                super.onIdle();
-                removeShortcutKeypress(jexer.TKeypress.kbAltT);
-                removeShortcutKeypress(jexer.TKeypress.kbF6);
-            }
-
-        };
-
-        // The initial window size was stock VT100 80x24.  Change that now,
-        // and then call onResize() to notify ptypipe to set the shell's
-        // window size.
-        tile.setWidth(width);
-        tile.setHeight(height);
-        tile.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
-                tile.getWidth(), tile.getHeight()));
-
-        return tile;
-    }
-
-}
diff --git a/examples/JexerTilingWindowManager2.java b/examples/JexerTilingWindowManager2.java
deleted file mode 100644 (file)
index 2a1512d..0000000
+++ /dev/null
@@ -1,175 +0,0 @@
-import jexer.TAction;
-import jexer.TApplication;
-import jexer.TDesktop;
-import jexer.TTerminalWidget;
-import jexer.TSplitPane;
-import jexer.TWidget;
-import jexer.event.TMenuEvent;
-import jexer.menu.TMenu;
-
-/**
- * Implements a simple tiling window manager.  A terminal widget is added to
- * the desktop, which can be split horizontally or vertically.  A close
- * action is provided to each window to remove the split when its shell
- * exits.
- *
- * This example shows what can be done with minimal changes to stock Jexer
- * widgets.
- */
-public class JexerTilingWindowManager2 extends TApplication {
-
-    /**
-     * Menu item: split the terminal vertically.
-     */
-    private static final int MENU_SPLIT_VERTICAL = 2000;
-
-    /**
-     * Menu item: split the terminal horizontally.
-     */
-    private static final int MENU_SPLIT_HORIZONTAL = 2001;
-    /**
-     * Menu item: recreate the root terminal.
-     */
-    private static final int MENU_RESPAWN_ROOT = 2002;
-
-    /**
-     * Handle to the root widget.
-     */
-    private TWidget root = null;
-
-    /**
-     * Main entry point.
-     */
-    public static void main(String [] args) throws Exception {
-        // For this application, we must use ptypipe so that the terminal
-        // shells can be aware of their size.
-        System.setProperty("jexer.TTerminal.ptypipe", "true");
-
-        // Let's also suppress the status line.
-        System.setProperty("jexer.hideStatusBar", "true");
-
-        JexerTilingWindowManager2 jtwm = new JexerTilingWindowManager2();
-        (new Thread(jtwm)).start();
-    }
-
-    /**
-     * Public constructor chooses the ECMA-48 / Xterm backend.
-     */
-    public JexerTilingWindowManager2() throws Exception {
-        super(BackendType.XTERM);
-
-        // The stock tool menu has items for redrawing the screen, opening
-        // images, and (when using the Swing backend) setting the font.
-        addToolMenu();
-
-        // We will have one menu containing a mix of new and stock commands
-        TMenu tileMenu = addMenu("&Tile");
-
-        // New commands for this example: split vertical and horizontal.
-        tileMenu.addItem(MENU_SPLIT_VERTICAL, "&Vertical Split");
-        tileMenu.addItem(MENU_SPLIT_HORIZONTAL, "&Horizontal Split");
-        tileMenu.addItem(MENU_RESPAWN_ROOT, "&Respawn Root Terminal");
-
-        // Stock commands: a new shell with resizable window, and exit
-        // program.
-        tileMenu.addSeparator();
-        tileMenu.addItem(TMenu.MID_SHELL, "&New Windowed Terminal");
-        tileMenu.addSeparator();
-        tileMenu.addDefaultItem(TMenu.MID_EXIT);
-
-        // TTerminalWidget can request the text-block mouse pointer be
-        // suppressed, but the default TDesktop will ignore it.  Let's set a
-        // new TDesktop to pass that mouse pointer visibility option to
-        // TApplication.
-        setDesktop(new TDesktop(this) {
-            @Override
-            public boolean hasHiddenMouse() {
-                TWidget active = getActiveChild();
-                if (active instanceof TTerminalWidget) {
-                    return ((TTerminalWidget) active).hasHiddenMouse();
-                }
-                return false;
-            }
-        });
-
-        // Spin up the root terminal
-        createRootTerminal();
-    }
-
-    /**
-     * Process menu events.
-     */
-    @Override
-    protected boolean onMenu(TMenuEvent event) {
-        TWidget active = getDesktop().getActiveChild();
-        TSplitPane split = null;
-
-        switch (event.getId()) {
-        case MENU_RESPAWN_ROOT:
-            assert (root == null);
-            createRootTerminal();
-            return true;
-
-        case MENU_SPLIT_VERTICAL:
-            if (root == null) {
-                assert (getDesktop().getActiveChild() == null);
-                createRootTerminal();
-                return true;
-            }
-            split = active.splitVertical(false, createTerminal());
-            if (active == root) {
-                root = split;
-            }
-            return true;
-
-        case MENU_SPLIT_HORIZONTAL:
-            if (root == null) {
-                assert (getDesktop().getActiveChild() == null);
-                createRootTerminal();
-                return true;
-            }
-            split = active.splitHorizontal(false, createTerminal());
-            if (active == root) {
-                root = split;
-            }
-            return true;
-
-        default:
-            return super.onMenu(event);
-        }
-
-    }
-
-    /**
-     * Create the root terminal.
-     */
-    private void createRootTerminal() {
-        assert (root == null);
-        disableMenuItem(MENU_RESPAWN_ROOT);
-        root = createTerminal();
-    }
-
-    /**
-     * Create a new terminal.
-     *
-     * @return the new terminal
-     */
-    private TWidget createTerminal() {
-        return new TTerminalWidget(getDesktop(), 0, 0,
-            getDesktop().getWidth(), getDesktop().getHeight(),
-            new TAction() {
-                public void DO() {
-                    if (source.getParent() instanceof TSplitPane) {
-                        ((TSplitPane) source.getParent()).removeSplit(source,
-                            true);
-                    } else {
-                        source.getApplication().enableMenuItem(
-                                MENU_RESPAWN_ROOT);
-                        source.remove();
-                        root = null;
-                    }
-                }
-            });
-    }
-
-}
diff --git a/examples/MyApplication.java b/examples/MyApplication.java
deleted file mode 100644 (file)
index 2af892c..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-import jexer.TApplication;
-
-public class MyApplication extends TApplication {
-
-    public MyApplication() throws Exception {
-        super(BackendType.XTERM);
-
-        // Create standard menus for Tool, File, and Window.
-        addToolMenu();
-        addFileMenu();
-        addWindowMenu();
-    }
-
-    public static void main(String [] args) throws Exception {
-        MyApplication app = new MyApplication();
-        app.run();
-    }
-}
diff --git a/examples/imgls b/examples/imgls
new file mode 100755 (executable)
index 0000000..99bbb9c
--- /dev/null
@@ -0,0 +1,64 @@
+#!/bin/bash
+
+# This is a modified version of the 'imgls' from iTerm2 located at
+# https://iterm2.com/utilities/imgls, modified to emit images with the
+# Jexer image protocol.
+
+# tmux requires unrecognized OSC sequences to be wrapped with DCS tmux;
+# <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
+
diff --git a/help/HelpFile.java b/help/HelpFile.java
new file mode 100644 (file)
index 0000000..7a6f49e
--- /dev/null
@@ -0,0 +1,381 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ResourceBundle;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.ParserConfigurationException;
+import org.xml.sax.SAXException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * A HelpFile is a collection of Topics with a table of contents and index of
+ * relevant terms.
+ */
+public class HelpFile {
+
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(HelpFile.class.getName());
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The XML factory.
+     */
+    private static DocumentBuilder domBuilder;
+
+    /**
+     * The map of topics by title.
+     */
+    private HashMap<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);
+        }
+    }
+
+}
diff --git a/help/HelpFile.properties b/help/HelpFile.properties
new file mode 100644 (file)
index 0000000..803961b
--- /dev/null
@@ -0,0 +1,3 @@
+tableOfContents=Table Of Contents
+index=Index
+searchResults=Search Results - {0}
diff --git a/help/Link.java b/help/Link.java
new file mode 100644 (file)
index 0000000..665381c
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.ResourceBundle;
+
+/**
+ * A Link is a section of text with a reference to a Topic.
+ */
+public class Link {
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The topic id that this link points to.
+     */
+    private String topic;
+
+    /**
+     * The text inside the link tag.
+     */
+    private String text;
+
+    /**
+     * The number of words in this link.
+     */
+    private int wordCount;
+
+    /**
+     * The word number (from the beginning of topic text) that corresponds to
+     * the first word of this link.
+     */
+    private int index;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param topic the topic to point to
+     * @param text the text inside the link tag
+     * @param index the word count index
+     */
+    public Link(final String topic, final String text, final int index) {
+        this.topic = topic;
+        this.text = text;
+        this.index = index;
+        this.wordCount = text.split("\\s+").length;
+    }
+
+    // ------------------------------------------------------------------------
+    // Link -------------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the topic.
+     *
+     * @return the topic
+     */
+    public String getTopic() {
+        return topic;
+    }
+
+    /**
+     * Get the link text.
+     *
+     * @return the text inside the link tag
+     */
+    public String getText() {
+        return text;
+    }
+
+    /**
+     * Get the word index for this link.
+     *
+     * @return the word number (from the beginning of topic text) that
+     * corresponds to the first word of this link
+     */
+    public int getIndex() {
+        return index;
+    }
+
+    /**
+     * Get the number of words in this link.
+     *
+     * @return the number of words in this link
+     */
+    public int getWordCount() {
+        return wordCount;
+    }
+
+    /**
+     * Generate a human-readable string for this widget.
+     *
+     * @return a human-readable string
+     */
+    @Override
+    public String toString() {
+        return String.format("%s(%8x) topic %s link text %s word # %d count %d",
+            getClass().getName(), hashCode(), topic, text, index, wordCount);
+    }
+
+}
diff --git a/help/THelpText.java b/help/THelpText.java
new file mode 100644 (file)
index 0000000..2e0afcf
--- /dev/null
@@ -0,0 +1,389 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jexer.THelpWindow;
+import jexer.TScrollableWidget;
+import jexer.TVScroller;
+import jexer.TWidget;
+import jexer.bits.CellAttributes;
+import jexer.bits.StringUtils;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
+import static jexer.TKeypress.*;
+
+/**
+ * THelpText displays help text with clickable links in a scrollable text
+ * area. It reflows automatically on resize.
+ */
+public class THelpText extends TScrollableWidget {
+
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The number of lines to scroll on mouse wheel up/down.
+     */
+    private static final int wheelScrollSize = 3;
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The paragraphs in this text box.
+     */
+    private List<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();
+    }
+
+}
diff --git a/help/TParagraph.java b/help/TParagraph.java
new file mode 100644 (file)
index 0000000..04559da
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.util.List;
+
+import jexer.TWidget;
+
+/**
+ * TParagraph contains a reflowable collection of TWords, some of which are
+ * clickable links.
+ */
+public class TParagraph extends TWidget {
+
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Topic text and links converted to words.
+     */
+    private List<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;
+    }
+
+
+}
diff --git a/help/TWord.java b/help/TWord.java
new file mode 100644 (file)
index 0000000..d46a22e
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import jexer.THelpWindow;
+import jexer.TWidget;
+import jexer.bits.CellAttributes;
+import jexer.bits.StringUtils;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
+import static jexer.TKeypress.*;
+
+/**
+ * TWord contains either a string to display or a clickable link.
+ */
+public class TWord extends TWidget {
+
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The word(s) to display.
+     */
+    private String words;
+
+    /**
+     * Link to another Topic.
+     */
+    private Link link;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param words the words to display
+     * @param link link to other topic, or null
+     */
+    public TWord(final String words, final Link link) {
+
+        // TWord is created by THelpText before the TParagraph is belongs to
+        // is created, so pass null as parent for now.
+        super(null, 0, 0, StringUtils.width(words), 1);
+
+        this.words = words;
+        this.link = link;
+
+        // Don't make text-only words "active".
+        if (link == null) {
+            setEnabled(false);
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Handle mouse press events.
+     *
+     * @param mouse mouse button press event
+     */
+    @Override
+    public void onMouseDown(final TMouseEvent mouse) {
+        if (mouse.isMouse1()) {
+            if (link != null) {
+                ((THelpWindow) getWindow()).setHelpTopic(link.getTopic());
+            }
+        }
+    }
+
+    /**
+     * Handle keystrokes.
+     *
+     * @param keypress keystroke event
+     */
+    @Override
+    public void onKeypress(final TKeypressEvent keypress) {
+        if (keypress.equals(kbEnter)) {
+            if (link != null) {
+                ((THelpWindow) getWindow()).setHelpTopic(link.getTopic());
+            }
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw the words.
+     */
+    @Override
+    public void draw() {
+        CellAttributes color = getTheme().getColor("thelpwindow.text");
+        if (link != null) {
+            if (isAbsoluteActive()) {
+                color = getTheme().getColor("thelpwindow.link.active");
+            } else {
+                color = getTheme().getColor("thelpwindow.link");
+            }
+        }
+        putStringXY(0, 0, words, color);
+    }
+
+    // ------------------------------------------------------------------------
+    // TWord ------------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+}
diff --git a/help/Topic.java b/help/Topic.java
new file mode 100644 (file)
index 0000000..8c0bc71
--- /dev/null
@@ -0,0 +1,339 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.ResourceBundle;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A Topic is a page of help text with a title and possibly links to other
+ * Topics.
+ */
+public class Topic implements Comparable<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)
+
+    }
+
+}
diff --git a/help/Topic.properties b/help/Topic.properties
new file mode 100644 (file)
index 0000000..1c8de6f
--- /dev/null
@@ -0,0 +1,2 @@
+topicNotFoundTitle=Topic Not Found
+topicNotFoundText=The help topic was not found.
diff --git a/help/package-info.java b/help/package-info.java
new file mode 100644 (file)
index 0000000..409c370
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+
+/**
+ * Online help system.
+ */
+package jexer.help;
similarity index 99%
rename from src/jexer/io/TimeoutInputStream.java
rename to io/TimeoutInputStream.java
index 3d8cdb0312494293b5e90bdeee39c5f21b32f659..70faff4eabaa0f932352b1886ea5651bfc424ac9 100644 (file)
@@ -244,7 +244,7 @@ public class TimeoutInputStream extends InputStream {
 
         if (timeoutMillis == 0) {
             // Block on the read().
-            return stream.read(b);
+            return stream.read(b, off, len);
         }
 
         int remaining = len;
similarity index 97%
rename from src/jexer/layout/StretchLayoutManager.java
rename to layout/StretchLayoutManager.java
index ee2bf5aba5e5f70d4da15e6376bcd36164b26f7e..4bcb0cffc4c29e3a6cb088efa7a8afa8f5c4bebd 100644 (file)
@@ -146,11 +146,11 @@ public class StretchLayoutManager implements LayoutManager {
      */
     private void layoutChildren() {
         double widthRatio = (double) width / originalWidth;
-        if (!Double.isFinite(widthRatio)) {
+        if (Math.abs(widthRatio) > Double.MAX_VALUE) {
             widthRatio = 1;
         }
         double heightRatio = (double) height / originalHeight;
-        if (!Double.isFinite(heightRatio)) {
+        if (Math.abs(heightRatio) > Double.MAX_VALUE) {
             heightRatio = 1;
         }
         for (TWidget child: children.keySet()) {
similarity index 94%
rename from src/jexer/menu/TMenu.java
rename to menu/TMenu.java
index 6d746df0c42ebc3f6da6ede018a33ff439233235..6a875c7c8377f154e9cafb07b064cde7ab889d3d 100644 (file)
@@ -72,10 +72,12 @@ public class TMenu extends TWindow {
     public static final int MID_SHELL           = 13;
 
     // Edit menu
-    public static final int MID_CUT             = 20;
-    public static final int MID_COPY            = 21;
-    public static final int MID_PASTE           = 22;
-    public static final int MID_CLEAR           = 23;
+    public static final int MID_UNDO            = 20;
+    public static final int MID_REDO            = 21;
+    public static final int MID_CUT             = 22;
+    public static final int MID_COPY            = 23;
+    public static final int MID_PASTE           = 24;
+    public static final int MID_CLEAR           = 25;
 
     // Search menu
     public static final int MID_FIND            = 30;
@@ -152,6 +154,11 @@ public class TMenu extends TWindow {
      */
     private MnemonicString mnemonic;
 
+    /**
+     * If true, draw icons with menu items.  Note package private access.
+     */
+    boolean useIcons = false;
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -182,6 +189,11 @@ public class TMenu extends TWindow {
         setHeight(2);
 
         setActive(false);
+
+        if (System.getProperty("jexer.menuIcons", "false").equals("true")) {
+            useIcons = true;
+        }
+
     }
 
     // ------------------------------------------------------------------------
@@ -446,7 +458,7 @@ public class TMenu extends TWindow {
         final boolean enabled) {
 
         assert (id >= 1024);
-        return addItemInternal(id, label, null, enabled);
+        return addItemInternal(id, label, null, enabled, -1);
     }
 
     /**
@@ -492,7 +504,7 @@ public class TMenu extends TWindow {
     private TMenuItem addItemInternal(final int id, final String label,
         final TKeypress key) {
 
-        return addItemInternal(id, label, key, true);
+        return addItemInternal(id, label, key, true, -1);
     }
 
     /**
@@ -502,15 +514,16 @@ public class TMenu extends TWindow {
      * @param label menu item label
      * @param key global keyboard accelerator
      * @param enabled default state for enabled
+     * @param icon icon picture/emoji
      * @return the new menu item
      */
     private TMenuItem addItemInternal(final int id, final String label,
-        final TKeypress key, final boolean enabled) {
+        final TKeypress key, final boolean enabled, final int icon) {
 
         int newY = getChildren().size() + 1;
         assert (newY < getHeight());
 
-        TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
+        TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label, icon);
         menuItem.setKey(key);
         menuItem.setEnabled(enabled);
         setHeight(getHeight() + 1);
@@ -551,6 +564,7 @@ public class TMenu extends TWindow {
 
         String label;
         TKeypress key = null;
+        int icon = -1;
         boolean checkable = false;
         boolean checked = false;
 
@@ -558,6 +572,7 @@ public class TMenu extends TWindow {
 
         case MID_REPAINT:
             label = i18n.getString("menuRepaintDesktop");
+            icon = 0x1F3A8;
             break;
 
         case MID_VIEW_IMAGE:
@@ -570,41 +585,56 @@ public class TMenu extends TWindow {
 
         case MID_NEW:
             label = i18n.getString("menuNew");
+            icon = 0x1F5CE;
             break;
 
         case MID_EXIT:
             label = i18n.getString("menuExit");
             key = kbAltX;
+            icon = 0x1F5D9;
             break;
 
         case MID_SHELL:
             label = i18n.getString("menuShell");
+            icon = 0x1F5AE;
             break;
 
         case MID_OPEN_FILE:
             label = i18n.getString("menuOpen");
             key = kbF3;
+            icon = 0x1F5C1;
             break;
 
+        case MID_UNDO:
+            label = i18n.getString("menuUndo");
+            key = kbCtrlZ;
+            break;
+        case MID_REDO:
+            label = i18n.getString("menuRedo");
+            key = kbCtrlY;
+            break;
         case MID_CUT:
             label = i18n.getString("menuCut");
             key = kbCtrlX;
+            icon = 0x1F5F6;
             break;
         case MID_COPY:
             label = i18n.getString("menuCopy");
             key = kbCtrlC;
+            icon = 0x1F5D0;
             break;
         case MID_PASTE:
             label = i18n.getString("menuPaste");
             key = kbCtrlV;
+            icon = 0x1F4CB;
             break;
         case MID_CLEAR:
             label = i18n.getString("menuClear");
-            // key = kbDel;
             break;
 
         case MID_FIND:
             label = i18n.getString("menuFind");
+            icon = 0x1F50D;
             break;
         case MID_REPLACE:
             label = i18n.getString("menuReplace");
@@ -622,6 +652,7 @@ public class TMenu extends TWindow {
             break;
         case MID_CASCADE:
             label = i18n.getString("menuWindowCascade");
+            icon = 0x1F5D7;
             break;
         case MID_CLOSE_ALL:
             label = i18n.getString("menuWindowCloseAll");
@@ -629,18 +660,22 @@ public class TMenu extends TWindow {
         case MID_WINDOW_MOVE:
             label = i18n.getString("menuWindowMove");
             key = kbCtrlF5;
+            icon = 0x263C;
             break;
         case MID_WINDOW_ZOOM:
             label = i18n.getString("menuWindowZoom");
             key = kbF5;
+            icon = 0x2195;
             break;
         case MID_WINDOW_NEXT:
             label = i18n.getString("menuWindowNext");
             key = kbF6;
+            icon = 0x2192;
             break;
         case MID_WINDOW_PREVIOUS:
             label = i18n.getString("menuWindowPrevious");
             key = kbShiftF6;
+            icon = 0x2190;
             break;
         case MID_WINDOW_CLOSE:
             label = i18n.getString("menuWindowClose");
@@ -775,7 +810,7 @@ public class TMenu extends TWindow {
             throw new IllegalArgumentException("Invalid menu ID: " + id);
         }
 
-        TMenuItem item = addItemInternal(id, label, key, enabled);
+        TMenuItem item = addItemInternal(id, label, key, enabled, icon);
         item.setCheckable(checkable);
         return item;
     }
similarity index 98%
rename from src/jexer/menu/TMenu.properties
rename to menu/TMenu.properties
index 4a0f8e6f6fef8b301b7467be871263b5858639cc..692293eb57f98bd7da88c66488c2b611c2fe69cc 100644 (file)
@@ -2,6 +2,8 @@ menuNew=&New
 menuExit=E&xit
 menuShell=O&S Shell
 menuOpen=&Open
+menuUndo=&Undo
+menuRedo=&Redo
 menuCut=Cu&t
 menuCopy=&Copy
 menuPaste=&Paste
similarity index 85%
rename from src/jexer/menu/TMenuItem.java
rename to menu/TMenuItem.java
index d9dfc2ac5482b64123713f45e8b58f7f9abf3e3d..b478059c077d17916dffff4b6b864f22874cbaf6 100644 (file)
@@ -80,6 +80,11 @@ public class TMenuItem extends TWidget {
      */
     private MnemonicString mnemonic;
 
+    /**
+     * An optional 2-cell-wide picture/icon for this item.
+     */
+    private int icon = -1;
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -96,6 +101,22 @@ public class TMenuItem extends TWidget {
     TMenuItem(final TMenu parent, final int id, final int x, final int y,
         final String label) {
 
+        this(parent, id, x, y, label, -1);
+    }
+
+    /**
+     * Package private constructor.
+     *
+     * @param parent parent widget
+     * @param id menu id
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param label menu item title
+     * @param icon icon picture/emoji
+     */
+    TMenuItem(final TMenu parent, final int id, final int x, final int y,
+        final String label, final int icon) {
+
         // Set parent and window
         super(parent);
 
@@ -105,8 +126,13 @@ public class TMenuItem extends TWidget {
         setY(y);
         setHeight(1);
         this.label = mnemonic.getRawLabel();
-        setWidth(StringUtils.width(label) + 4);
+        if (parent.useIcons) {
+            setWidth(StringUtils.width(label) + 6);
+        } else {
+            setWidth(StringUtils.width(label) + 4);
+        }
         this.id = id;
+        this.icon = icon;
 
         // Default state for some known menu items
         switch (id) {
@@ -220,26 +246,31 @@ public class TMenuItem extends TWidget {
             }
         }
 
+        boolean useIcons = ((TMenu) getParent()).useIcons;
+
         char cVSide = GraphicsChars.WINDOW_SIDE;
         vLineXY(0, 0, 1, cVSide, background);
         vLineXY(getWidth() - 1, 0, 1, cVSide, background);
 
         hLineXY(1, 0, getWidth() - 2, ' ', menuColor);
-        putStringXY(2, 0, mnemonic.getRawLabel(), menuColor);
+        putStringXY(2 + (useIcons ? 2 : 0), 0, mnemonic.getRawLabel(),
+            menuColor);
         if (key != null) {
             String keyLabel = key.toString();
             putStringXY((getWidth() - StringUtils.width(keyLabel) - 2), 0,
                 keyLabel, menuColor);
         }
         if (mnemonic.getScreenShortcutIdx() >= 0) {
-            putCharXY(2 + mnemonic.getScreenShortcutIdx(), 0,
-                mnemonic.getShortcut(), menuMnemonicColor);
+            putCharXY(2 + (useIcons ? 2 : 0) + mnemonic.getScreenShortcutIdx(),
+                0, mnemonic.getShortcut(), menuMnemonicColor);
         }
         if (checked) {
             assert (checkable);
             putCharXY(1, 0, GraphicsChars.CHECK, menuColor);
         }
-
+        if ((useIcons == true) && (icon != -1)) {
+            putCharXY(2, 0, icon, menuColor);
+        }
     }
 
     // ------------------------------------------------------------------------
@@ -318,12 +349,34 @@ public class TMenuItem extends TWidget {
         if (key != null) {
             int newWidth = (StringUtils.width(label) + 4 +
                 StringUtils.width(key.toString()) + 2);
+            if (((TMenu) getParent()).useIcons) {
+                newWidth += 2;
+            }
             if (newWidth > getWidth()) {
                 setWidth(newWidth);
             }
         }
     }
 
+    /**
+     * Get a picture/emoji icon for this menu item.
+     *
+     * @return the codepoint, or -1 if no icon is specified for this menu
+     * item
+     */
+    public final int getIcon() {
+        return icon;
+    }
+
+    /**
+     * Set a picture/emoji icon for this menu item.
+     *
+     * @param icon a codepoint, or -1 to unset the icon
+     */
+    public final void setIcon(final int icon) {
+        this.icon = icon;
+    }
+
     /**
      * Dispatch event(s) due to selection or click.
      */
similarity index 90%
rename from src/jexer/menu/TSubMenu.java
rename to menu/TSubMenu.java
index e285c5ab6d78c485f67ed6e2597c4c17e31af189..be281b52d97d58741072632e9009e5dc1c344904 100644 (file)
@@ -212,6 +212,21 @@ public class TSubMenu extends TMenuItem {
         return menu.addItem(id, label, key);
     }
 
+    /**
+     * Convenience function to add a custom menu item.
+     *
+     * @param id menu item ID.  Must be greater than 1024.
+     * @param label menu item label
+     * @param key global keyboard accelerator
+     * @param enabled default state for enabled
+     * @return the new menu item
+     */
+    public TMenuItem addItem(final int id, final String label,
+        final TKeypress key, final boolean enabled) {
+
+        return menu.addItem(id, label, key, enabled);
+    }
+
     /**
      * Convenience function to add a menu item.
      *
@@ -223,6 +238,20 @@ public class TSubMenu extends TMenuItem {
         return menu.addItem(id, label);
     }
 
+    /**
+     * Convenience function to add a menu item.
+     *
+     * @param id menu item ID.  Must be greater than 1024.
+     * @param label menu item label
+     * @param enabled default state for enabled
+     * @return the new menu item
+     */
+    public TMenuItem addItem(final int id, final String label,
+        final boolean enabled) {
+
+        return menu.addItem(id, label, enabled);
+    }
+
     /**
      * Convenience function to add one of the default menu items.
      *
diff --git a/pom.xml b/pom.xml
deleted file mode 100644 (file)
index 17ca174..0000000
--- a/pom.xml
+++ /dev/null
@@ -1,216 +0,0 @@
-<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>
diff --git a/resources/help.xml b/resources/help.xml
new file mode 100644 (file)
index 0000000..7c68c02
--- /dev/null
@@ -0,0 +1,60 @@
+<?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>
diff --git a/screenshots/jexer_sixel_in_sixel.png b/screenshots/jexer_sixel_in_sixel.png
deleted file mode 100644 (file)
index 3b3c35d..0000000
Binary files a/screenshots/jexer_sixel_in_sixel.png and /dev/null differ
diff --git a/screenshots/new_demo1.png b/screenshots/new_demo1.png
deleted file mode 100644 (file)
index 675afeb..0000000
Binary files a/screenshots/new_demo1.png and /dev/null differ
diff --git a/screenshots/readme_application.png b/screenshots/readme_application.png
deleted file mode 100644 (file)
index 6e3eed8..0000000
Binary files a/screenshots/readme_application.png and /dev/null differ
diff --git a/screenshots/sixel_color_wheel.png b/screenshots/sixel_color_wheel.png
deleted file mode 100644 (file)
index d4ce5f7..0000000
Binary files a/screenshots/sixel_color_wheel.png and /dev/null differ
diff --git a/screenshots/sixel_images.png b/screenshots/sixel_images.png
deleted file mode 100644 (file)
index c66e56e..0000000
Binary files a/screenshots/sixel_images.png and /dev/null differ
diff --git a/screenshots/snake_swing.png b/screenshots/snake_swing.png
deleted file mode 100644 (file)
index 85871f9..0000000
Binary files a/screenshots/snake_swing.png and /dev/null differ
diff --git a/screenshots/snake_xterm.png b/screenshots/snake_xterm.png
deleted file mode 100644 (file)
index 4c74874..0000000
Binary files a/screenshots/snake_xterm.png and /dev/null differ
diff --git a/screenshots/yodawg.png b/screenshots/yodawg.png
deleted file mode 100644 (file)
index a7d3d31..0000000
Binary files a/screenshots/yodawg.png and /dev/null differ
diff --git a/src/jexer/TEditorWidget.java b/src/jexer/TEditorWidget.java
deleted file mode 100644 (file)
index a694533..0000000
+++ /dev/null
@@ -1,546 +0,0 @@
-/*
- * Jexer - Java Text User Interface
- *
- * The MIT License (MIT)
- *
- * Copyright (C) 2019 Kevin Lamonte
- *
- * Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
- * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- *
- * @author Kevin Lamonte [kevin.lamonte@gmail.com]
- * @version 1
- */
-package jexer;
-
-import java.io.IOException;
-
-import jexer.bits.CellAttributes;
-import jexer.event.TKeypressEvent;
-import jexer.event.TMouseEvent;
-import jexer.event.TResizeEvent;
-import jexer.teditor.Document;
-import jexer.teditor.Line;
-import jexer.teditor.Word;
-import static jexer.TKeypress.*;
-
-/**
- * TEditorWidget displays an editable text document.  It is unaware of
- * scrolling behavior, but can respond to mouse and keyboard events.
- */
-public class TEditorWidget extends TWidget {
-
-    // ------------------------------------------------------------------------
-    // Constants --------------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * The number of lines to scroll on mouse wheel up/down.
-     */
-    private static final int wheelScrollSize = 3;
-
-    // ------------------------------------------------------------------------
-    // Variables --------------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * The document being edited.
-     */
-    private Document document;
-
-    /**
-     * The default color for the TEditor class.
-     */
-    private CellAttributes defaultColor = null;
-
-    /**
-     * The topmost line number in the visible area.  0-based.
-     */
-    private int topLine = 0;
-
-    /**
-     * The leftmost column number in the visible area.  0-based.
-     */
-    private int leftColumn = 0;
-
-    // ------------------------------------------------------------------------
-    // Constructors -----------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Public constructor.
-     *
-     * @param parent parent widget
-     * @param text text on the screen
-     * @param x column relative to parent
-     * @param y row relative to parent
-     * @param width width of text area
-     * @param height height of text area
-     */
-    public TEditorWidget(final TWidget parent, final String text, final int x,
-        final int y, final int width, final int height) {
-
-        // Set parent and window
-        super(parent, x, y, width, height);
-
-        setCursorVisible(true);
-
-        defaultColor = getTheme().getColor("teditor");
-        document = new Document(text, defaultColor);
-    }
-
-    // ------------------------------------------------------------------------
-    // TWidget ----------------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Draw the text box.
-     */
-    @Override
-    public void draw() {
-        for (int i = 0; i < getHeight(); i++) {
-            // Background line
-            getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor);
-
-            // Now draw document's line
-            if (topLine + i < document.getLineCount()) {
-                Line line = document.getLine(topLine + i);
-                int x = 0;
-                for (Word word: line.getWords()) {
-                    // For now, we are cheating: draw outside the left region
-                    // if needed and let screen do the clipping.
-                    getScreen().putStringXY(x - leftColumn, i, word.getText(),
-                        word.getColor());
-                    x += word.getDisplayLength();
-                    if (x - leftColumn > getWidth()) {
-                        break;
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Handle mouse press events.
-     *
-     * @param mouse mouse button press event
-     */
-    @Override
-    public void onMouseDown(final TMouseEvent mouse) {
-        if (mouse.isMouseWheelUp()) {
-            for (int i = 0; i < wheelScrollSize; i++) {
-                if (topLine > 0) {
-                    topLine--;
-                    alignDocument(false);
-                }
-            }
-            return;
-        }
-        if (mouse.isMouseWheelDown()) {
-            for (int i = 0; i < wheelScrollSize; i++) {
-                if (topLine < document.getLineCount() - 1) {
-                    topLine++;
-                    alignDocument(true);
-                }
-            }
-            return;
-        }
-
-        if (mouse.isMouse1()) {
-            // Set the row and column
-            int newLine = topLine + mouse.getY();
-            int newX = leftColumn + mouse.getX();
-            if (newLine > document.getLineCount() - 1) {
-                // Go to the end
-                document.setLineNumber(document.getLineCount() - 1);
-                document.end();
-                if (newLine > document.getLineCount() - 1) {
-                    setCursorY(document.getLineCount() - 1 - topLine);
-                } else {
-                    setCursorY(mouse.getY());
-                }
-                alignCursor();
-                return;
-            }
-
-            document.setLineNumber(newLine);
-            setCursorY(mouse.getY());
-            if (newX >= document.getCurrentLine().getDisplayLength()) {
-                document.end();
-                alignCursor();
-            } else {
-                document.setCursor(newX);
-                setCursorX(mouse.getX());
-            }
-            return;
-        }
-
-        // Pass to children
-        super.onMouseDown(mouse);
-    }
-
-    /**
-     * Handle keystrokes.
-     *
-     * @param keypress keystroke event
-     */
-    @Override
-    public void onKeypress(final TKeypressEvent keypress) {
-        if (keypress.equals(kbLeft)) {
-            document.left();
-            alignTopLine(false);
-        } else if (keypress.equals(kbRight)) {
-            document.right();
-            alignTopLine(true);
-        } else if (keypress.equals(kbAltLeft)
-            || keypress.equals(kbCtrlLeft)
-        ) {
-            document.backwardsWord();
-            alignTopLine(false);
-        } else if (keypress.equals(kbAltRight)
-            || keypress.equals(kbCtrlRight)
-        ) {
-            document.forwardsWord();
-            alignTopLine(true);
-        } else if (keypress.equals(kbUp)) {
-            document.up();
-            alignTopLine(false);
-        } else if (keypress.equals(kbDown)) {
-            document.down();
-            alignTopLine(true);
-        } else if (keypress.equals(kbPgUp)) {
-            document.up(getHeight() - 1);
-            alignTopLine(false);
-        } else if (keypress.equals(kbPgDn)) {
-            document.down(getHeight() - 1);
-            alignTopLine(true);
-        } else if (keypress.equals(kbHome)) {
-            if (document.home()) {
-                leftColumn = 0;
-                if (leftColumn < 0) {
-                    leftColumn = 0;
-                }
-                setCursorX(0);
-            }
-        } else if (keypress.equals(kbEnd)) {
-            if (document.end()) {
-                alignCursor();
-            }
-        } else if (keypress.equals(kbCtrlHome)) {
-            document.setLineNumber(0);
-            document.home();
-            topLine = 0;
-            leftColumn = 0;
-            setCursorX(0);
-            setCursorY(0);
-        } else if (keypress.equals(kbCtrlEnd)) {
-            document.setLineNumber(document.getLineCount() - 1);
-            document.end();
-            alignTopLine(false);
-        } else if (keypress.equals(kbIns)) {
-            document.setOverwrite(!document.getOverwrite());
-        } else if (keypress.equals(kbDel)) {
-            document.del();
-            alignCursor();
-        } else if (keypress.equals(kbBackspace)
-            || keypress.equals(kbBackspaceDel)
-        ) {
-            document.backspace();
-            alignTopLine(false);
-        } else if (keypress.equals(kbTab)) {
-            // TODO: tab character.  For now just add spaces until we hit
-            // modulo 8.
-            for (int i = document.getCursor(); (i + 1) % 8 != 0; i++) {
-                document.addChar(' ');
-            }
-            alignCursor();
-        } else if (keypress.equals(kbEnter)) {
-            document.enter();
-            alignTopLine(true);
-        } else if (!keypress.getKey().isFnKey()
-            && !keypress.getKey().isAlt()
-            && !keypress.getKey().isCtrl()
-        ) {
-            // Plain old keystroke, process it
-            document.addChar(keypress.getKey().getChar());
-            alignCursor();
-        } else {
-            // Pass other keys (tab etc.) on to TWidget
-            super.onKeypress(keypress);
-        }
-    }
-
-    /**
-     * Method that subclasses can override to handle window/screen resize
-     * events.
-     *
-     * @param resize resize event
-     */
-    @Override
-    public void onResize(final TResizeEvent resize) {
-        // Change my width/height, and pull the cursor in as needed.
-        if (resize.getType() == TResizeEvent.Type.WIDGET) {
-            setWidth(resize.getWidth());
-            setHeight(resize.getHeight());
-            // See if the cursor is now outside the window, and if so move
-            // things.
-            if (getCursorX() >= getWidth()) {
-                leftColumn += getCursorX() - (getWidth() - 1);
-                setCursorX(getWidth() - 1);
-            }
-            if (getCursorY() >= getHeight()) {
-                topLine += getCursorY() - (getHeight() - 1);
-                setCursorY(getHeight() - 1);
-            }
-        } else {
-            // Let superclass handle it
-            super.onResize(resize);
-        }
-    }
-
-    // ------------------------------------------------------------------------
-    // TEditorWidget ----------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Align visible area with document current line.
-     *
-     * @param topLineIsTop if true, make the top visible line the document
-     * current line if it was off-screen.  If false, make the bottom visible
-     * line the document current line.
-     */
-    private void alignTopLine(final boolean topLineIsTop) {
-        int line = document.getLineNumber();
-
-        if ((line < topLine) || (line > topLine + getHeight() - 1)) {
-            // Need to move topLine to bring document back into view.
-            if (topLineIsTop) {
-                topLine = line - (getHeight() - 1);
-                if (topLine < 0) {
-                    topLine = 0;
-                }
-                assert (topLine >= 0);
-            } else {
-                topLine = line;
-                assert (topLine >= 0);
-            }
-        }
-
-        /*
-        System.err.println("line " + line + " topLine " + topLine);
-        */
-
-        // Document is in view, let's set cursorY
-        assert (line >= topLine);
-        setCursorY(line - topLine);
-        alignCursor();
-    }
-
-    /**
-     * Align document current line with visible area.
-     *
-     * @param topLineIsTop if true, make the top visible line the document
-     * current line if it was off-screen.  If false, make the bottom visible
-     * line the document current line.
-     */
-    private void alignDocument(final boolean topLineIsTop) {
-        int line = document.getLineNumber();
-        int cursor = document.getCursor();
-
-        if ((line < topLine) || (line > topLine + getHeight() - 1)) {
-            // Need to move document to ensure it fits view.
-            if (topLineIsTop) {
-                document.setLineNumber(topLine);
-            } else {
-                document.setLineNumber(topLine + (getHeight() - 1));
-            }
-            if (cursor < document.getCurrentLine().getDisplayLength()) {
-                document.setCursor(cursor);
-            }
-        }
-
-        /*
-        System.err.println("getLineNumber() " + document.getLineNumber() +
-            " topLine " + topLine);
-        */
-
-        // Document is in view, let's set cursorY
-        setCursorY(document.getLineNumber() - topLine);
-        alignCursor();
-    }
-
-    /**
-     * Align visible cursor with document cursor.
-     */
-    private void alignCursor() {
-        int width = getWidth();
-
-        int desiredX = document.getCursor() - leftColumn;
-        if (desiredX < 0) {
-            // We need to push the screen to the left.
-            leftColumn = document.getCursor();
-        } else if (desiredX > width - 1) {
-            // We need to push the screen to the right.
-            leftColumn = document.getCursor() - (width - 1);
-        }
-
-        /*
-        System.err.println("document cursor " + document.getCursor() +
-            " leftColumn " + leftColumn);
-        */
-
-
-        setCursorX(document.getCursor() - leftColumn);
-    }
-
-    /**
-     * Get the number of lines in the underlying Document.
-     *
-     * @return the number of lines
-     */
-    public int getLineCount() {
-        return document.getLineCount();
-    }
-
-    /**
-     * Get the current visible top row number.  1-based.
-     *
-     * @return the visible top row number.  Row 1 is the first row.
-     */
-    public int getVisibleRowNumber() {
-        return topLine + 1;
-    }
-
-    /**
-     * Set the current visible row number.  1-based.
-     *
-     * @param row the new visible row number.  Row 1 is the first row.
-     */
-    public void setVisibleRowNumber(final int row) {
-        assert (row > 0);
-        if ((row > 0) && (row < document.getLineCount())) {
-            topLine = row - 1;
-            alignDocument(true);
-        }
-    }
-
-    /**
-     * Get the current editing row number.  1-based.
-     *
-     * @return the editing row number.  Row 1 is the first row.
-     */
-    public int getEditingRowNumber() {
-        return document.getLineNumber() + 1;
-    }
-
-    /**
-     * Set the current editing row number.  1-based.
-     *
-     * @param row the new editing row number.  Row 1 is the first row.
-     */
-    public void setEditingRowNumber(final int row) {
-        assert (row > 0);
-        if ((row > 0) && (row < document.getLineCount())) {
-            document.setLineNumber(row - 1);
-            alignTopLine(true);
-        }
-    }
-
-    /**
-     * Set the current visible column number.  1-based.
-     *
-     * @return the visible column number.  Column 1 is the first column.
-     */
-    public int getVisibleColumnNumber() {
-        return leftColumn + 1;
-    }
-
-    /**
-     * Set the current visible column number.  1-based.
-     *
-     * @param column the new visible column number.  Column 1 is the first
-     * column.
-     */
-    public void setVisibleColumnNumber(final int column) {
-        assert (column > 0);
-        if ((column > 0) && (column < document.getLineLengthMax())) {
-            leftColumn = column - 1;
-            alignDocument(true);
-        }
-    }
-
-    /**
-     * Get the current editing column number.  1-based.
-     *
-     * @return the editing column number.  Column 1 is the first column.
-     */
-    public int getEditingColumnNumber() {
-        return document.getCursor() + 1;
-    }
-
-    /**
-     * Set the current editing column number.  1-based.
-     *
-     * @param column the new editing column number.  Column 1 is the first
-     * column.
-     */
-    public void setEditingColumnNumber(final int column) {
-        if ((column > 0) && (column < document.getLineLength())) {
-            document.setCursor(column - 1);
-            alignCursor();
-        }
-    }
-
-    /**
-     * Get the maximum possible row number.  1-based.
-     *
-     * @return the maximum row number.  Row 1 is the first row.
-     */
-    public int getMaximumRowNumber() {
-        return document.getLineCount() + 1;
-    }
-
-    /**
-     * Get the maximum possible column number.  1-based.
-     *
-     * @return the maximum column number.  Column 1 is the first column.
-     */
-    public int getMaximumColumnNumber() {
-        return document.getLineLengthMax() + 1;
-    }
-
-    /**
-     * Get the dirty value.
-     *
-     * @return true if the buffer is dirty
-     */
-    public boolean isDirty() {
-        return document.isDirty();
-    }
-
-    /**
-     * Save contents to file.
-     *
-     * @param filename file to save to
-     * @throws IOException if a java.io operation throws
-     */
-    public void saveToFilename(final String filename) throws IOException {
-        document.saveToFilename(filename);
-    }
-
-}
similarity index 77%
rename from src/jexer/teditor/Document.java
rename to teditor/Document.java
index 2abfef6635f3c1877fc733ee36ea8c67d01160b6..b4a9a3bfb6b3ab476d29b45b3a01b4fbdca77816 100644 (file)
@@ -76,6 +76,23 @@ public class Document {
      */
     private Highlighter highlighter = new Highlighter();
 
+    /**
+     * The tab stop size.
+     */
+    private int tabSize = 8;
+
+    /**
+     * If true, backspace at an indent level goes back a full indent level.
+     * If false, backspace always goes back one column.
+     */
+    private boolean backspaceUnindents = false;
+
+    /**
+     * If true, save files with tab characters.  If false, convert tabs to
+     * spaces when saving files.
+     */
+    private boolean saveWithTabs = false;
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -89,7 +106,8 @@ public class Document {
     public Document(final String str, final CellAttributes defaultColor) {
         this.defaultColor = defaultColor;
 
-        // TODO: set different colors based on file extension
+        // Set colors to resemble the Borland IDE colors, but for Java
+        // language keywords.
         highlighter.setJavaColors();
 
         String [] rawLines = str.split("\n");
@@ -98,16 +116,41 @@ public class Document {
         }
     }
 
+    /**
+     * Private constructor used by dup().
+     */
+    private Document() {
+        // NOP
+    }
+
     // ------------------------------------------------------------------------
     // Document ---------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Create a duplicate instance.
+     *
+     * @return duplicate intance
+     */
+    public Document dup() {
+        Document other = new Document();
+        for (Line line: lines) {
+            other.lines.add(line.dup());
+        }
+        other.lineNumber = lineNumber;
+        other.overwrite = overwrite;
+        other.dirty = dirty;
+        other.defaultColor = defaultColor;
+        other.highlighter.setTo(highlighter);
+        return other;
+    }
+
     /**
      * Get the overwrite flag.
      *
      * @return true if addChar() overwrites data, false if it inserts
      */
-    public boolean getOverwrite() {
+    public boolean isOverwrite() {
         return overwrite;
     }
 
@@ -120,6 +163,13 @@ public class Document {
         return dirty;
     }
 
+    /**
+     * Unset the dirty flag.
+     */
+    public void setNotDirty() {
+        dirty = false;
+    }
+
     /**
      * Save contents to file.
      *
@@ -133,7 +183,11 @@ public class Document {
                 "UTF-8");
 
             for (Line line: lines) {
-                output.write(line.getRawString());
+                if (saveWithTabs) {
+                    output.write(convertSpacesToTabs(line.getRawString()));
+                } else {
+                    output.write(line.getRawString());
+                }
                 output.write("\n");
             }
 
@@ -362,7 +416,7 @@ public class Document {
         // If at the beginning of a word already, push past it.
         if ((getChar() != -1)
             && (getRawLine().length() > 0)
-            && !Character.isSpace((char) getChar())
+            && !Character.isWhitespace((char) getChar())
         ) {
             left();
         }
@@ -370,7 +424,7 @@ public class Document {
         // int line = lineNumber;
         while ((getChar() == -1)
             || (getRawLine().length() == 0)
-            || Character.isSpace((char) getChar())
+            || Character.isWhitespace((char) getChar())
         ) {
             if (left() == false) {
                 return;
@@ -380,12 +434,12 @@ public class Document {
 
         assert (getChar() != -1);
 
-        if (!Character.isSpace((char) getChar())
+        if (!Character.isWhitespace((char) getChar())
             && (getRawLine().length() > 0)
         ) {
             // Advance until at the beginning of the document or a whitespace
             // is encountered.
-            while (!Character.isSpace((char) getChar())) {
+            while (!Character.isWhitespace((char) getChar())) {
                 int line = lineNumber;
                 if (left() == false) {
                     // End of document, bail out.
@@ -418,7 +472,7 @@ public class Document {
             }
             if (lineNumber != line) {
                 // We wrapped a line.  Here that counts as whitespace.
-                if (!Character.isSpace((char) getChar())) {
+                if (!Character.isWhitespace((char) getChar())) {
                     // We found a character immediately after the line.
                     // Done!
                     return;
@@ -429,12 +483,12 @@ public class Document {
         }
         assert (getChar() != -1);
 
-        if (!Character.isSpace((char) getChar())
+        if (!Character.isWhitespace((char) getChar())
             && (getRawLine().length() > 0)
         ) {
             // Advance until at the end of the document or a whitespace is
             // encountered.
-            while (!Character.isSpace((char) getChar())) {
+            while (!Character.isWhitespace((char) getChar())) {
                 line = lineNumber;
                 if (right() == false) {
                     // End of document, bail out.
@@ -442,7 +496,7 @@ public class Document {
                 }
                 if (lineNumber != line) {
                     // We wrapped a line.  Here that counts as whitespace.
-                    if (!Character.isSpace((char) getChar())
+                    if (!Character.isWhitespace((char) getChar())
                         && (getRawLine().length() > 0)
                     ) {
                         // We found a character immediately after the line.
@@ -462,7 +516,7 @@ public class Document {
             }
             if (lineNumber != line) {
                 // We wrapped a line.  Here that counts as whitespace.
-                if (!Character.isSpace((char) getChar())) {
+                if (!Character.isWhitespace((char) getChar())) {
                     // We found a character immediately after the line.
                     // Done!
                     return;
@@ -473,10 +527,10 @@ public class Document {
         }
         assert (getChar() != -1);
 
-        if (Character.isSpace((char) getChar())) {
+        if (Character.isWhitespace((char) getChar())) {
             // Advance until at the end of the document or a non-whitespace
             // is encountered.
-            while (Character.isSpace((char) getChar())) {
+            while (Character.isWhitespace((char) getChar())) {
                 if (right() == false) {
                     // End of document, bail out.
                     return;
@@ -543,7 +597,7 @@ public class Document {
         dirty = true;
         int cursor = lines.get(lineNumber).getCursor();
         if (cursor > 0) {
-            lines.get(lineNumber).backspace();
+            lines.get(lineNumber).backspace(tabSize, backspaceUnindents);
         } else if (lineNumber > 0) {
             // Join two lines
             lineNumber--;
@@ -595,6 +649,62 @@ public class Document {
         }
     }
 
+    /**
+     * Get the tab stop size.
+     *
+     * @return the tab stop size
+     */
+    public int getTabSize() {
+        return tabSize;
+    }
+
+    /**
+     * Set the tab stop size.
+     *
+     * @param tabSize the new tab stop size
+     */
+    public void setTabSize(final int tabSize) {
+        this.tabSize = tabSize;
+    }
+
+    /**
+     * Set the backspace unindent option.
+     *
+     * @param backspaceUnindents If true, backspace at an indent level goes
+     * back a full indent level.  If false, backspace always goes back one
+     * column.
+     */
+    public void setBackspaceUnindents(final boolean backspaceUnindents) {
+        this.backspaceUnindents = backspaceUnindents;
+    }
+
+    /**
+     * Set the save with tabs option.
+     *
+     * @param saveWithTabs If true, save files with tab characters.  If
+     * false, convert tabs to spaces when saving files.
+     */
+    public void setSaveWithTabs(final boolean saveWithTabs) {
+        this.saveWithTabs = saveWithTabs;
+    }
+
+    /**
+     * Handle the tab character.
+     */
+    public void tab() {
+        if (overwrite) {
+            del();
+        }
+        lines.get(lineNumber).tab(tabSize);
+    }
+
+    /**
+     * Handle the backtab (shift-tab) character.
+     */
+    public void backTab() {
+        lines.get(lineNumber).backTab(tabSize);
+    }
+
     /**
      * Get a (shallow) copy of the list of lines.
      *
@@ -637,4 +747,77 @@ public class Document {
         return lines.get(lineNumber).getDisplayLength();
     }
 
+    /**
+     * Get the entire contents of the document as one string.
+     *
+     * @return the document contents
+     */
+    public String getText() {
+        StringBuilder sb = new StringBuilder();
+        for (Line line: getLines()) {
+            sb.append(line.getRawString());
+            sb.append("\n");
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Trim trailing whitespace from lines and trailing empty
+     * lines from the document.
+     */
+    public void cleanWhitespace() {
+        for (Line line: getLines()) {
+            line.trimRight();
+        }
+        if (lines.size() == 0) {
+            return;
+        }
+        while (lines.get(lines.size() - 1).length() == 0) {
+            lines.remove(lines.size() - 1);
+        }
+        if (lineNumber > lines.size() - 1) {
+            lineNumber = lines.size() - 1;
+        }
+    }
+
+    /**
+     * Set keyword highlighting.
+     *
+     * @param enabled if true, enable keyword highlighting
+     */
+    public void setHighlighting(final boolean enabled) {
+        highlighter.setEnabled(enabled);
+        for (Line line: getLines()) {
+            line.scanLine();
+        }
+    }
+
+    /**
+     * Convert a string with leading spaces to a mix of tabs and spaces.
+     *
+     * @param string the string to convert
+     */
+    private String convertSpacesToTabs(final String string) {
+        if (string.length() == 0) {
+            return string;
+        }
+
+        int start = 0;
+        while (string.charAt(start) == ' ') {
+            start++;
+        }
+        int tabCount = start / 8;
+        if (tabCount == 0) {
+            return string;
+        }
+
+        StringBuilder sb = new StringBuilder(string.length());
+
+        for (int i = 0; i < tabCount; i++) {
+            sb.append('\t');
+        }
+        sb.append(string.substring(tabCount * 8));
+        return sb.toString();
+    }
+
 }
similarity index 76%
rename from src/jexer/teditor/Highlighter.java
rename to teditor/Highlighter.java
index a48419455e541697e12da7dfd81ebb8db52d7e9e..23ee90014e863bc0568a3526f4e086a793b6465c 100644 (file)
@@ -56,13 +56,36 @@ public class Highlighter {
      * 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.
      *
@@ -87,7 +110,10 @@ public class Highlighter {
      * @return color associated with name, e.g. bold yellow on blue
      */
     public CellAttributes getColor(final String name) {
-        CellAttributes attr = (CellAttributes) colors.get(name);
+        if (colors == null) {
+            return null;
+        }
+        CellAttributes attr = colors.get(name);
         return attr;
     }
 
@@ -95,19 +121,41 @@ public class Highlighter {
      * Sets to defaults that resemble the Borland IDE colors.
      */
     public void setJavaColors() {
+        colors = new TreeMap<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) {
similarity index 70%
rename from src/jexer/teditor/Line.java
rename to teditor/Line.java
index 7cd5febabee8462f6c51bc66e886903795aff698..b5c980a59f9c9812b6f6a84ce832fe9781051702 100644 (file)
@@ -32,6 +32,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 import jexer.bits.CellAttributes;
+import jexer.bits.GraphicsChars;
 import jexer.bits.StringUtils;
 
 /**
@@ -92,7 +93,31 @@ public class Line {
 
         this.defaultColor = defaultColor;
         this.highlighter = highlighter;
-        this.rawText = new StringBuilder(str);
+
+        this.rawText = new StringBuilder();
+        int col = 0;
+        for (int i = 0; i < str.length(); i++) {
+            char ch = str.charAt(i);
+            if (ch == '\t') {
+                // Expand tabs
+                int j = col % 8;
+                do {
+                    rawText.append(' ');
+                    j++;
+                    col++;
+                } while ((j % 8) != 0);
+                continue;
+            }
+            if ((ch <= 0x20) || (ch == 0x7F)) {
+                // Replace all other C0 bytes with CP437 glyphs.
+                rawText.append(GraphicsChars.CP437[(int) ch]);
+                col++;
+                continue;
+            }
+
+            rawText.append(ch);
+            col++;
+        }
 
         scanLine();
     }
@@ -107,10 +132,33 @@ public class Line {
         this(str, defaultColor, null);
     }
 
+    /**
+     * Private constructor used by dup().
+     */
+    private Line() {
+        // NOP
+    }
+
     // ------------------------------------------------------------------------
     // Line -------------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Create a duplicate instance.
+     *
+     * @return duplicate intance
+     */
+    public Line dup() {
+        Line other = new Line();
+        other.defaultColor = defaultColor;
+        other.highlighter = highlighter;
+        other.position = position;
+        other.screenPosition = screenPosition;
+        other.rawText = new StringBuilder(rawText);
+        other.scanLine();
+        return other;
+    }
+
     /**
      * Get a (shallow) copy of the words in this line.
      *
@@ -193,9 +241,19 @@ public class Line {
     }
 
     /**
-     * Scan rawText and make words out of it.
+     * Get the raw length of this line.
+     *
+     * @return the length of this line in characters, which may be different
+     * from the number of cells needed to display it
+     */
+    public int length() {
+        return rawText.length();
+    }
+
+    /**
+     * Scan rawText and make words out of it.  Note package private access.
      */
-    private void scanLine() {
+    void scanLine() {
         words.clear();
         Word word = new Word(this.defaultColor, this.highlighter);
         words.add(word);
@@ -236,7 +294,7 @@ public class Line {
         if (getDisplayLength() == 0) {
             return false;
         }
-        if (position == getDisplayLength() - 1) {
+        if (screenPosition == getDisplayLength() - 1) {
             return false;
         }
         if (position < rawText.length()) {
@@ -267,7 +325,7 @@ public class Line {
      * @return true if the cursor position changed
      */
     public boolean end() {
-        if (position != getDisplayLength() - 1) {
+        if (screenPosition != getDisplayLength() - 1) {
             position = rawText.length();
             screenPosition = StringUtils.width(rawText.toString());
             return true;
@@ -281,7 +339,7 @@ public class Line {
     public void del() {
         assert (words.size() > 0);
 
-        if (position < getDisplayLength()) {
+        if (screenPosition < getDisplayLength()) {
             int n = Character.charCount(rawText.codePointAt(position));
             for (int i = 0; i < n; i++) {
                 rawText.deleteCharAt(position);
@@ -294,8 +352,32 @@ public class Line {
 
     /**
      * Delete the character immediately preceeding the cursor.
+     *
+     * @param tabSize the tab stop size
+     * @param backspaceUnindents If true, backspace at an indent level goes
+     * back a full indent level.  If false, backspace always goes back one
+     * column.
      */
-    public void backspace() {
+    public void backspace(final int tabSize, final boolean backspaceUnindents) {
+        if ((backspaceUnindents == true)
+            && (tabSize > 0)
+            && (screenPosition > 0)
+            && (rawText.charAt(position - 1) == ' ')
+            && ((screenPosition % tabSize) == 0)
+        ) {
+            boolean doBackTab = true;
+            for (int i = 0; i < position; i++) {
+                if (rawText.charAt(i) != ' ') {
+                    doBackTab = false;
+                    break;
+                }
+            }
+            if (doBackTab) {
+                backTab(tabSize);
+                return;
+            }
+        }
+
         if (left()) {
             del();
         }
@@ -307,7 +389,7 @@ public class Line {
      * @param ch the character to insert
      */
     public void addChar(final int ch) {
-        if (position < getDisplayLength() - 1) {
+        if (screenPosition < getDisplayLength() - 1) {
             rawText.insert(position, Character.toChars(ch));
         } else {
             rawText.append(Character.toChars(ch));
@@ -323,7 +405,7 @@ public class Line {
      * @param ch the character to replace
      */
     public void replaceChar(final int ch) {
-        if (position < getDisplayLength() - 1) {
+        if (screenPosition < getDisplayLength() - 1) {
             // Replace character
             String oldText = rawText.toString();
             rawText = new StringBuilder(oldText.substring(0, position));
@@ -345,7 +427,7 @@ public class Line {
      * @param screenPosition the position on screen
      * @return the equivalent position in text
      */
-    protected int screenToTextPosition(final int screenPosition) {
+    private int screenToTextPosition(final int screenPosition) {
         if (screenPosition == 0) {
             return 0;
         }
@@ -362,4 +444,55 @@ public class Line {
             " exceeds available text length " + rawText.length());
     }
 
+    /**
+     * Trim trailing whitespace from line, repositioning cursor if needed.
+     */
+    public void trimRight() {
+        if (rawText.length() == 0) {
+            return;
+        }
+        if (!Character.isWhitespace(rawText.charAt(rawText.length() - 1))) {
+            return;
+        }
+        while ((rawText.length() > 0)
+            && Character.isWhitespace(rawText.charAt(rawText.length() - 1))
+        ) {
+            rawText.deleteCharAt(rawText.length() - 1);
+        }
+        if (position >= rawText.length()) {
+            end();
+        }
+        scanLine();
+    }
+
+    /**
+     * Handle the tab character.
+     *
+     * @param tabSize the tab stop size
+     */
+    public void tab(final int tabSize) {
+        if (tabSize > 0) {
+            do {
+                addChar(' ');
+            } while ((screenPosition % tabSize) != 0);
+        }
+    }
+
+    /**
+     * Handle the backtab (shift-tab) character.
+     *
+     * @param tabSize the tab stop size
+     */
+    public void backTab(final int tabSize) {
+        if ((tabSize > 0) && (screenPosition > 0)
+            && (rawText.charAt(position - 1) == ' ')
+        ) {
+            do {
+                backspace(tabSize, false);
+            } while (((screenPosition % tabSize) != 0)
+                && (screenPosition > 0)
+                && (rawText.charAt(position - 1) == ' '));
+        }
+    }
+
 }
similarity index 96%
rename from src/jexer/teditor/Word.java
rename to teditor/Word.java
index eada29cff83ed8b1c61c59d1741b5646d1f645b8..483f9c3d86c46a1dfbf225876eb7b42c219ba0c1 100644 (file)
@@ -135,11 +135,6 @@ public class Word {
      * @return the number of cells needed to display this word
      */
     public int getDisplayLength() {
-        // For now, just use the text length.  In the future, this will be a
-        // grapheme count.
-
-        // TODO: figure out how to handle the tab character.  Do we have a
-        // global tab stops list and current word position?
         return StringUtils.width(text.toString());
     }
 
similarity index 92%
rename from src/jexer/tterminal/DisplayLine.java
rename to tterminal/DisplayLine.java
index 06a05a330ddccb50cc3ecf142cea0c4c87c38df9..87e6952fb515447b43500395d40cb8e80d0c919b 100644 (file)
@@ -248,4 +248,29 @@ public class DisplayLine {
         chars[chars.length - 1] = new Cell(newCell);
     }
 
+    /**
+     * Determine if line contains image data.
+     *
+     * @return true if the line has image data
+     */
+    public boolean isImage() {
+        for (int i = 0; i < chars.length; i++) {
+            if (chars[i].isImage()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Clear image data from line.
+     */
+    public void clearImages() {
+        for (int i = 0; i < chars.length; i++) {
+            if (chars[i].isImage()) {
+                chars[i].reset();
+            }
+        }
+    }
+
 }
similarity index 91%
rename from src/jexer/tterminal/ECMA48.java
rename to tterminal/ECMA48.java
index 1d3481169cc5300c5134c652e7745a962f42a2ec..537b2e0a4a3ba25238ee5d935f393aa07fcba24c 100644 (file)
  */
 package jexer.tterminal;
 
-import java.awt.Graphics2D;
+import java.awt.Graphics;
 import java.awt.image.BufferedImage;
+import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
 import java.io.CharArrayWriter;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -45,6 +47,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import javax.imageio.ImageIO;
 
 import jexer.TKeypress;
 import jexer.backend.GlyphMaker;
@@ -256,7 +259,7 @@ public class ECMA48 implements Runnable {
     /**
      * The type of emulator to be.
      */
-    private DeviceType type = DeviceType.VT102;
+    private final DeviceType type;
 
     /**
      * The scrollback buffer characters + attributes.
@@ -271,7 +274,7 @@ public class ECMA48 implements Runnable {
     /**
      * The maximum number of lines in the scrollback buffer.
      */
-    private int maxScrollback = 10000;
+    private int scrollbackMax = 10000;
 
     /**
      * The terminal's input.  For type == XTERM, this is an InputStreamReader
@@ -323,29 +326,29 @@ public class ECMA48 implements Runnable {
      * Physical display width.  We start at 80x24, but the user can resize us
      * bigger/smaller.
      */
-    private int width;
+    private int width = 80;
 
     /**
      * Physical display height.  We start at 80x24, but the user can resize
      * us bigger/smaller.
      */
-    private int height;
+    private int height = 24;
 
     /**
      * Top margin of the scrolling region.
      */
-    private int scrollRegionTop;
+    private int scrollRegionTop = 0;
 
     /**
      * Bottom margin of the scrolling region.
      */
-    private int scrollRegionBottom;
+    private int scrollRegionBottom = height - 1;
 
     /**
      * Right margin column number.  This can be selected by the remote side
      * to be 80/132 (rightMargin values 79/131), or it can be (width - 1).
      */
-    private int rightMargin;
+    private int rightMargin = 79;
 
     /**
      * Last character printed.
@@ -357,7 +360,7 @@ public class ECMA48 implements Runnable {
      * 132), but the line does NOT wrap until another character is written to
      * column 1 of the next line, after which the cursor moves to column 2.
      */
-    private boolean wrapLineFlag;
+    private boolean wrapLineFlag = false;
 
     /**
      * VT220 single shift flag.
@@ -394,7 +397,7 @@ public class ECMA48 implements Runnable {
     /**
      * Non-csi collect buffer.
      */
-    private StringBuilder collectBuffer;
+    private StringBuilder collectBuffer = new StringBuilder(128);
 
     /**
      * When true, use the G1 character set.
@@ -469,7 +472,7 @@ public class ECMA48 implements Runnable {
     /**
      * Sixel collection buffer.
      */
-    private StringBuilder sixelParseBuffer;
+    private StringBuilder sixelParseBuffer = new StringBuilder(2048);
 
     /**
      * Sixel shared palette.
@@ -503,6 +506,11 @@ public class ECMA48 implements Runnable {
      */
     private ArrayList<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.
@@ -655,7 +663,8 @@ public class ECMA48 implements Runnable {
             this.inputStream  = new TimeoutInputStream(inputStream, 2000);
         }
         if (type == DeviceType.XTERM) {
-            this.input    = new InputStreamReader(this.inputStream, "UTF-8");
+            this.input    = new InputStreamReader(new BufferedInputStream(
+                this.inputStream, 1024 * 128), "UTF-8");
             this.output   = new OutputStreamWriter(new
                 BufferedOutputStream(outputStream), "UTF-8");
             this.outputStream = null;
@@ -669,6 +678,8 @@ public class ECMA48 implements Runnable {
         for (int i = 0; i < height; i++) {
             display.add(new DisplayLine(currentState.attr));
         }
+        assert (currentState.cursorY < height);
+        assert (currentState.cursorX < width);
 
         // Spin up the input reader
         readerThread = new Thread(this);
@@ -760,11 +771,28 @@ public class ECMA48 implements Runnable {
                                 int ch = Character.codePointAt(readBufferUTF8,
                                     i);
                                 i += Character.charCount(ch);
-                                consume(ch);
+
+                                // Special case for VT10x: 7-bit characters
+                                // only.
+                                if ((type == DeviceType.VT100)
+                                    || (type == DeviceType.VT102)
+                                ) {
+                                    consume(ch & 0x7F);
+                                } else {
+                                    consume(ch);
+                                }
                             }
                         } else {
                             for (int i = 0; i < rc; i++) {
-                                consume(readBuffer[i]);
+                                // Special case for VT10x: 7-bit characters
+                                // only.
+                                if ((type == DeviceType.VT100)
+                                    || (type == DeviceType.VT102)
+                                ) {
+                                    consume(readBuffer[i] & 0x7F);
+                                } else {
+                                    consume(readBuffer[i]);
+                                }
                             }
                         }
                     }
@@ -831,6 +859,34 @@ public class ECMA48 implements Runnable {
     // ECMA48 -----------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Wait for a period of time to get output from the launched process.
+     *
+     * @param millis millis to wait for, or 0 to wait forever
+     * @return true if the launched process has emitted something
+     */
+    public boolean waitForOutput(final int millis) {
+        if (millis < 0) {
+            throw new IllegalArgumentException("timeout must be >= 0");
+        }
+        int waitedMillis = millis;
+        final int pollTimeout = 5;
+        while (true) {
+            if (readCount != 0) {
+                return true;
+            }
+            if ((millis > 0) && (waitedMillis < 0)){
+                return false;
+            }
+            try {
+                Thread.sleep(pollTimeout);
+            } catch (InterruptedException e) {
+                // SQUASH
+            }
+            waitedMillis -= pollTimeout;
+        }
+    }
+
     /**
      * Process keyboard and mouse events from the user.
      *
@@ -874,14 +930,14 @@ public class ECMA48 implements Runnable {
 
         case VT220:
         case XTERM:
-            // "I am a VT220" - 7 bit version
+            // "I am a VT220" - 7 bit version, with sixel and Jexer image
+            // support.
             if (!s8c1t) {
-                return "\033[?62;1;6;9;4;22c";
-                // return "\033[?62;1;6;9;4;22;444c";
+                return "\033[?62;1;6;9;4;22;444c";
             }
-            // "I am a VT220" - 8 bit version
-            return "\u009b?62;1;6;9;4;22c";
-            // return "\u009b?62;1;6;9;4;22;444c";
+            // "I am a VT220" - 8 bit version, with sixel and Jexer image
+            // support.
+            return "\u009b?62;1;6;9;4;22;444c";
         default:
             throw new IllegalArgumentException("Invalid device type: " + type);
         }
@@ -1002,11 +1058,6 @@ public class ECMA48 implements Runnable {
         // the input streams.
         if (stopReaderThread == false) {
             stopReaderThread = true;
-            try {
-                readerThread.join(1000);
-            } catch (InterruptedException e) {
-                // SQUASH
-            }
         }
 
         // Now close the output stream.
@@ -1186,8 +1237,8 @@ public class ECMA48 implements Runnable {
         int delta = height - this.height;
         this.height = height;
         scrollRegionBottom += delta;
-        if (scrollRegionBottom < 0) {
-            scrollRegionBottom = height;
+        if ((scrollRegionBottom < 0) || (scrollRegionTop > height - 1)) {
+            scrollRegionBottom = height - 1;
         }
         if (scrollRegionTop >= scrollRegionBottom) {
             scrollRegionTop = 0;
@@ -1204,10 +1255,29 @@ public class ECMA48 implements Runnable {
             display.add(line);
         }
         while (display.size() > height) {
-            scrollback.add(display.remove(0));
+            appendScrollbackLine(display.remove(0));
         }
     }
 
+    /**
+     * Get the maximum number of lines in the scrollback buffer.
+     *
+     * @return the maximum number of lines in the scrollback buffer
+     */
+    public int getScrollbackMax() {
+        return scrollbackMax;
+    }
+
+    /**
+     * Set the maximum number of lines for the scrollback buffer.
+     *
+     * @param scrollbackMax the maximum number of lines for the scrollback
+     * buffer
+     */
+    public final void setScrollbackMax(final int scrollbackMax) {
+        this.scrollbackMax = scrollbackMax;
+    }
+
     /**
      * Get visible cursor flag.
      *
@@ -1242,7 +1312,7 @@ public class ECMA48 implements Runnable {
      */
     private void toGround() {
         csiParams.clear();
-        collectBuffer = new StringBuilder(8);
+        collectBuffer.setLength(0);
         scanState = ScanState.GROUND;
     }
 
@@ -1265,7 +1335,7 @@ public class ECMA48 implements Runnable {
             colors88.add(0);
         }
 
-        // Set default system colors.
+        // Set default system colors.  These match DOS colors.
         colors88.set(0, 0x00000000);
         colors88.set(1, 0x00a80000);
         colors88.set(2, 0x0000a800);
@@ -1283,6 +1353,249 @@ public class ECMA48 implements Runnable {
         colors88.set(13, 0x00fc54fc);
         colors88.set(14, 0x0054fcfc);
         colors88.set(15, 0x00fcfcfc);
+
+        // These match xterm's default colors from 256colres.h.
+        colors88.set(16, 0x000000);
+        colors88.set(17, 0x00005f);
+        colors88.set(18, 0x000087);
+        colors88.set(19, 0x0000af);
+        colors88.set(20, 0x0000d7);
+        colors88.set(21, 0x0000ff);
+        colors88.set(22, 0x005f00);
+        colors88.set(23, 0x005f5f);
+        colors88.set(24, 0x005f87);
+        colors88.set(25, 0x005faf);
+        colors88.set(26, 0x005fd7);
+        colors88.set(27, 0x005fff);
+        colors88.set(28, 0x008700);
+        colors88.set(29, 0x00875f);
+        colors88.set(30, 0x008787);
+        colors88.set(31, 0x0087af);
+        colors88.set(32, 0x0087d7);
+        colors88.set(33, 0x0087ff);
+        colors88.set(34, 0x00af00);
+        colors88.set(35, 0x00af5f);
+        colors88.set(36, 0x00af87);
+        colors88.set(37, 0x00afaf);
+        colors88.set(38, 0x00afd7);
+        colors88.set(39, 0x00afff);
+        colors88.set(40, 0x00d700);
+        colors88.set(41, 0x00d75f);
+        colors88.set(42, 0x00d787);
+        colors88.set(43, 0x00d7af);
+        colors88.set(44, 0x00d7d7);
+        colors88.set(45, 0x00d7ff);
+        colors88.set(46, 0x00ff00);
+        colors88.set(47, 0x00ff5f);
+        colors88.set(48, 0x00ff87);
+        colors88.set(49, 0x00ffaf);
+        colors88.set(50, 0x00ffd7);
+        colors88.set(51, 0x00ffff);
+        colors88.set(52, 0x5f0000);
+        colors88.set(53, 0x5f005f);
+        colors88.set(54, 0x5f0087);
+        colors88.set(55, 0x5f00af);
+        colors88.set(56, 0x5f00d7);
+        colors88.set(57, 0x5f00ff);
+        colors88.set(58, 0x5f5f00);
+        colors88.set(59, 0x5f5f5f);
+        colors88.set(60, 0x5f5f87);
+        colors88.set(61, 0x5f5faf);
+        colors88.set(62, 0x5f5fd7);
+        colors88.set(63, 0x5f5fff);
+        colors88.set(64, 0x5f8700);
+        colors88.set(65, 0x5f875f);
+        colors88.set(66, 0x5f8787);
+        colors88.set(67, 0x5f87af);
+        colors88.set(68, 0x5f87d7);
+        colors88.set(69, 0x5f87ff);
+        colors88.set(70, 0x5faf00);
+        colors88.set(71, 0x5faf5f);
+        colors88.set(72, 0x5faf87);
+        colors88.set(73, 0x5fafaf);
+        colors88.set(74, 0x5fafd7);
+        colors88.set(75, 0x5fafff);
+        colors88.set(76, 0x5fd700);
+        colors88.set(77, 0x5fd75f);
+        colors88.set(78, 0x5fd787);
+        colors88.set(79, 0x5fd7af);
+        colors88.set(80, 0x5fd7d7);
+        colors88.set(81, 0x5fd7ff);
+        colors88.set(82, 0x5fff00);
+        colors88.set(83, 0x5fff5f);
+        colors88.set(84, 0x5fff87);
+        colors88.set(85, 0x5fffaf);
+        colors88.set(86, 0x5fffd7);
+        colors88.set(87, 0x5fffff);
+        colors88.set(88, 0x870000);
+        colors88.set(89, 0x87005f);
+        colors88.set(90, 0x870087);
+        colors88.set(91, 0x8700af);
+        colors88.set(92, 0x8700d7);
+        colors88.set(93, 0x8700ff);
+        colors88.set(94, 0x875f00);
+        colors88.set(95, 0x875f5f);
+        colors88.set(96, 0x875f87);
+        colors88.set(97, 0x875faf);
+        colors88.set(98, 0x875fd7);
+        colors88.set(99, 0x875fff);
+        colors88.set(100, 0x878700);
+        colors88.set(101, 0x87875f);
+        colors88.set(102, 0x878787);
+        colors88.set(103, 0x8787af);
+        colors88.set(104, 0x8787d7);
+        colors88.set(105, 0x8787ff);
+        colors88.set(106, 0x87af00);
+        colors88.set(107, 0x87af5f);
+        colors88.set(108, 0x87af87);
+        colors88.set(109, 0x87afaf);
+        colors88.set(110, 0x87afd7);
+        colors88.set(111, 0x87afff);
+        colors88.set(112, 0x87d700);
+        colors88.set(113, 0x87d75f);
+        colors88.set(114, 0x87d787);
+        colors88.set(115, 0x87d7af);
+        colors88.set(116, 0x87d7d7);
+        colors88.set(117, 0x87d7ff);
+        colors88.set(118, 0x87ff00);
+        colors88.set(119, 0x87ff5f);
+        colors88.set(120, 0x87ff87);
+        colors88.set(121, 0x87ffaf);
+        colors88.set(122, 0x87ffd7);
+        colors88.set(123, 0x87ffff);
+        colors88.set(124, 0xaf0000);
+        colors88.set(125, 0xaf005f);
+        colors88.set(126, 0xaf0087);
+        colors88.set(127, 0xaf00af);
+        colors88.set(128, 0xaf00d7);
+        colors88.set(129, 0xaf00ff);
+        colors88.set(130, 0xaf5f00);
+        colors88.set(131, 0xaf5f5f);
+        colors88.set(132, 0xaf5f87);
+        colors88.set(133, 0xaf5faf);
+        colors88.set(134, 0xaf5fd7);
+        colors88.set(135, 0xaf5fff);
+        colors88.set(136, 0xaf8700);
+        colors88.set(137, 0xaf875f);
+        colors88.set(138, 0xaf8787);
+        colors88.set(139, 0xaf87af);
+        colors88.set(140, 0xaf87d7);
+        colors88.set(141, 0xaf87ff);
+        colors88.set(142, 0xafaf00);
+        colors88.set(143, 0xafaf5f);
+        colors88.set(144, 0xafaf87);
+        colors88.set(145, 0xafafaf);
+        colors88.set(146, 0xafafd7);
+        colors88.set(147, 0xafafff);
+        colors88.set(148, 0xafd700);
+        colors88.set(149, 0xafd75f);
+        colors88.set(150, 0xafd787);
+        colors88.set(151, 0xafd7af);
+        colors88.set(152, 0xafd7d7);
+        colors88.set(153, 0xafd7ff);
+        colors88.set(154, 0xafff00);
+        colors88.set(155, 0xafff5f);
+        colors88.set(156, 0xafff87);
+        colors88.set(157, 0xafffaf);
+        colors88.set(158, 0xafffd7);
+        colors88.set(159, 0xafffff);
+        colors88.set(160, 0xd70000);
+        colors88.set(161, 0xd7005f);
+        colors88.set(162, 0xd70087);
+        colors88.set(163, 0xd700af);
+        colors88.set(164, 0xd700d7);
+        colors88.set(165, 0xd700ff);
+        colors88.set(166, 0xd75f00);
+        colors88.set(167, 0xd75f5f);
+        colors88.set(168, 0xd75f87);
+        colors88.set(169, 0xd75faf);
+        colors88.set(170, 0xd75fd7);
+        colors88.set(171, 0xd75fff);
+        colors88.set(172, 0xd78700);
+        colors88.set(173, 0xd7875f);
+        colors88.set(174, 0xd78787);
+        colors88.set(175, 0xd787af);
+        colors88.set(176, 0xd787d7);
+        colors88.set(177, 0xd787ff);
+        colors88.set(178, 0xd7af00);
+        colors88.set(179, 0xd7af5f);
+        colors88.set(180, 0xd7af87);
+        colors88.set(181, 0xd7afaf);
+        colors88.set(182, 0xd7afd7);
+        colors88.set(183, 0xd7afff);
+        colors88.set(184, 0xd7d700);
+        colors88.set(185, 0xd7d75f);
+        colors88.set(186, 0xd7d787);
+        colors88.set(187, 0xd7d7af);
+        colors88.set(188, 0xd7d7d7);
+        colors88.set(189, 0xd7d7ff);
+        colors88.set(190, 0xd7ff00);
+        colors88.set(191, 0xd7ff5f);
+        colors88.set(192, 0xd7ff87);
+        colors88.set(193, 0xd7ffaf);
+        colors88.set(194, 0xd7ffd7);
+        colors88.set(195, 0xd7ffff);
+        colors88.set(196, 0xff0000);
+        colors88.set(197, 0xff005f);
+        colors88.set(198, 0xff0087);
+        colors88.set(199, 0xff00af);
+        colors88.set(200, 0xff00d7);
+        colors88.set(201, 0xff00ff);
+        colors88.set(202, 0xff5f00);
+        colors88.set(203, 0xff5f5f);
+        colors88.set(204, 0xff5f87);
+        colors88.set(205, 0xff5faf);
+        colors88.set(206, 0xff5fd7);
+        colors88.set(207, 0xff5fff);
+        colors88.set(208, 0xff8700);
+        colors88.set(209, 0xff875f);
+        colors88.set(210, 0xff8787);
+        colors88.set(211, 0xff87af);
+        colors88.set(212, 0xff87d7);
+        colors88.set(213, 0xff87ff);
+        colors88.set(214, 0xffaf00);
+        colors88.set(215, 0xffaf5f);
+        colors88.set(216, 0xffaf87);
+        colors88.set(217, 0xffafaf);
+        colors88.set(218, 0xffafd7);
+        colors88.set(219, 0xffafff);
+        colors88.set(220, 0xffd700);
+        colors88.set(221, 0xffd75f);
+        colors88.set(222, 0xffd787);
+        colors88.set(223, 0xffd7af);
+        colors88.set(224, 0xffd7d7);
+        colors88.set(225, 0xffd7ff);
+        colors88.set(226, 0xffff00);
+        colors88.set(227, 0xffff5f);
+        colors88.set(228, 0xffff87);
+        colors88.set(229, 0xffffaf);
+        colors88.set(230, 0xffffd7);
+        colors88.set(231, 0xffffff);
+        colors88.set(232, 0x080808);
+        colors88.set(233, 0x121212);
+        colors88.set(234, 0x1c1c1c);
+        colors88.set(235, 0x262626);
+        colors88.set(236, 0x303030);
+        colors88.set(237, 0x3a3a3a);
+        colors88.set(238, 0x444444);
+        colors88.set(239, 0x4e4e4e);
+        colors88.set(240, 0x585858);
+        colors88.set(241, 0x626262);
+        colors88.set(242, 0x6c6c6c);
+        colors88.set(243, 0x767676);
+        colors88.set(244, 0x808080);
+        colors88.set(245, 0x8a8a8a);
+        colors88.set(246, 0x949494);
+        colors88.set(247, 0x9e9e9e);
+        colors88.set(248, 0xa8a8a8);
+        colors88.set(249, 0xb2b2b2);
+        colors88.set(250, 0xbcbcbc);
+        colors88.set(251, 0xc6c6c6);
+        colors88.set(252, 0xd0d0d0);
+        colors88.set(253, 0xdadada);
+        colors88.set(254, 0xe4e4e4);
+        colors88.set(255, 0xeeeeee);
+
     }
 
     /**
@@ -1357,8 +1670,13 @@ public class ECMA48 implements Runnable {
         currentState            = new SaveableState();
         savedState              = new SaveableState();
         scanState               = ScanState.GROUND;
-        width                   = 80;
-        height                  = 24;
+        if (displayListener != null) {
+            width = displayListener.getDisplayWidth();
+            height = displayListener.getDisplayHeight();
+        } else {
+            width               = 80;
+            height              = 24;
+        }
         scrollRegionTop         = 0;
         scrollRegionBottom      = height - 1;
         rightMargin             = width - 1;
@@ -1366,11 +1684,6 @@ public class ECMA48 implements Runnable {
         arrowKeyMode            = ArrowKeyMode.ANSI;
         keypadMode              = KeypadMode.Numeric;
         wrapLineFlag            = false;
-        if (displayListener != null) {
-            width = displayListener.getDisplayWidth();
-            height = displayListener.getDisplayHeight();
-            rightMargin         = width - 1;
-        }
 
         // Flags
         shiftOut                = false;
@@ -1401,14 +1714,25 @@ public class ECMA48 implements Runnable {
         toGround();
     }
 
+    /**
+     * Append a to the scrollback buffer, clearing image data for lines more
+     * than three screenfuls in.
+     */
+    private void appendScrollbackLine(DisplayLine line) {
+        scrollback.add(line);
+        if (scrollback.size() > height * 3) {
+            scrollback.get(scrollback.size() - (height * 3)).clearImages();
+        }
+    }
+
     /**
      * Append a new line to the bottom of the display, adding lines off the
      * top to the scrollback buffer.
      */
     private void newDisplayLine() {
         // Scroll the top line off into the scrollback buffer
-        scrollback.add(display.get(0));
-        if (scrollback.size() > maxScrollback) {
+        appendScrollbackLine(display.get(0));
+        while (scrollback.size() > scrollbackMax) {
             scrollback.remove(0);
             scrollback.trimToSize();
         }
@@ -1453,7 +1777,6 @@ public class ECMA48 implements Runnable {
      * Handle a linefeed.
      */
     private void linefeed() {
-
         if (currentState.cursorY < scrollRegionBottom) {
             // Increment screen y
             currentState.cursorY++;
@@ -1664,35 +1987,45 @@ public class ECMA48 implements Runnable {
         if (mouseEncoding == MouseEncoding.SGR) {
             sb.append((char) 0x1B);
             sb.append("[<");
+            int buttons = 0;
 
             if (mouse.isMouse1()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append("32;");
+                    buttons = 32;
                 } else {
-                    sb.append("0;");
+                    buttons = 0;
                 }
             } else if (mouse.isMouse2()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append("33;");
+                    buttons = 33;
                 } else {
-                    sb.append("1;");
+                    buttons = 1;
                 }
             } else if (mouse.isMouse3()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append("34;");
+                    buttons = 34;
                 } else {
-                    sb.append("2;");
+                    buttons = 2;
                 }
             } else if (mouse.isMouseWheelUp()) {
-                sb.append("64;");
+                buttons = 64;
             } else if (mouse.isMouseWheelDown()) {
-                sb.append("65;");
+                buttons = 65;
             } else {
                 // This is motion with no buttons down.
-                sb.append("35;");
+                buttons = 35;
+            }
+            if (mouse.isAlt()) {
+                buttons |= 0x08;
+            }
+            if (mouse.isCtrl()) {
+                buttons |= 0x10;
+            }
+            if (mouse.isShift()) {
+                buttons |= 0x04;
             }
 
-            sb.append(String.format("%d;%d", mouse.getX() + 1,
+            sb.append(String.format("%d;%d;%d", buttons, mouse.getX() + 1,
                     mouse.getY() + 1));
 
             if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
@@ -1706,35 +2039,46 @@ public class ECMA48 implements Runnable {
             sb.append((char) 0x1B);
             sb.append('[');
             sb.append('M');
+            int buttons = 0;
             if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
-                sb.append((char) (0x03 + 32));
+                buttons = 0x03 + 32;
             } else if (mouse.isMouse1()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append((char) (0x00 + 32 + 32));
+                    buttons = 0x00 + 32 + 32;
                 } else {
-                    sb.append((char) (0x00 + 32));
+                    buttons = 0x00 + 32;
                 }
             } else if (mouse.isMouse2()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append((char) (0x01 + 32 + 32));
+                    buttons = 0x01 + 32 + 32;
                 } else {
-                    sb.append((char) (0x01 + 32));
+                    buttons = 0x01 + 32;
                 }
             } else if (mouse.isMouse3()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append((char) (0x02 + 32 + 32));
+                    buttons = 0x02 + 32 + 32;
                 } else {
-                    sb.append((char) (0x02 + 32));
+                    buttons = 0x02 + 32;
                 }
             } else if (mouse.isMouseWheelUp()) {
-                sb.append((char) (0x04 + 64));
+                buttons = 0x04 + 64;
             } else if (mouse.isMouseWheelDown()) {
-                sb.append((char) (0x05 + 64));
+                buttons = 0x05 + 64;
             } else {
                 // This is motion with no buttons down.
-                sb.append((char) (0x03 + 32));
+                buttons = 0x03 + 32;
+            }
+            if (mouse.isAlt()) {
+                buttons |= 0x08;
+            }
+            if (mouse.isCtrl()) {
+                buttons |= 0x10;
+            }
+            if (mouse.isShift()) {
+                buttons |= 0x04;
             }
 
+            sb.append((char) (buttons & 0xFF));
             sb.append((char) (mouse.getX() + 33));
             sb.append((char) (mouse.getY() + 33));
         }
@@ -3156,10 +3500,10 @@ public class ECMA48 implements Runnable {
                     if (decPrivateModeFlag == true) {
                         if (value == true) {
                             // Enable sixel scrolling (default).
-                            // TODO
+                            // Not supported
                         } else {
                             // Disable sixel scrolling.
-                            // TODO
+                            // Not supported
                         }
                     }
                 }
@@ -3939,14 +4283,14 @@ public class ECMA48 implements Runnable {
                      * RGB color mode.
                      */
                     rgbColor = true;
-                    break;
+                    continue;
 
                 case 5:
                     /*
                      * Indexed color mode.
                      */
                     idx88Color = true;
-                    break;
+                    continue;
 
                 default:
                     /*
@@ -3994,7 +4338,7 @@ public class ECMA48 implements Runnable {
 
                 case 8:
                     // Invisible
-                    // TODO
+                    // Not supported
                     break;
 
                 case 90:
@@ -4379,6 +4723,9 @@ public class ECMA48 implements Runnable {
             // DECSTBM
             int top = getCsiParam(0, 1, 1, height) - 1;
             int bottom = getCsiParam(1, height, 1, height) - 1;
+            if (bottom > height - 1) {
+                bottom = height - 1;
+            }
 
             if (top > bottom) {
                 top = bottom;
@@ -4732,13 +5079,22 @@ public class ECMA48 implements Runnable {
     private void oscPut(final char xtermChar) {
         // System.err.println("oscPut: " + xtermChar);
 
+        boolean oscEnd = false;
+
+        if (xtermChar == 0x07) {
+            oscEnd = true;
+        }
+        if ((xtermChar == '\\')
+            && (collectBuffer.charAt(collectBuffer.length() - 1) == '\033')
+        ) {
+            oscEnd = true;
+        }
+
         // Collect first
         collectBuffer.append(xtermChar);
 
         // Xterm cases...
-        if ((xtermChar == 0x07)
-            || (collectBuffer.toString().endsWith("\033\\"))
-        ) {
+        if (oscEnd) {
             String args = null;
             if (xtermChar == 0x07) {
                 args = collectBuffer.substring(0, collectBuffer.length() - 1);
@@ -4792,11 +5148,18 @@ public class ECMA48 implements Runnable {
                     }
                 }
 
-                if (p[0].equals("444") && (p.length == 5)) {
-                    // Jexer image
-                    parseJexerImage(p[1], p[2], p[3], p[4]);
+                if (p[0].equals("444")) {
+                    if (p[1].equals("0") && (p.length == 6)) {
+                        // Jexer image - RGB
+                        parseJexerImageRGB(p[2], p[3], p[4], p[5]);
+                    } else if (p[1].equals("1") && (p.length == 4)) {
+                        // Jexer image - PNG
+                        parseJexerImageFile(1, p[2], p[3]);
+                    } else if (p[1].equals("2") && (p.length == 4)) {
+                        // Jexer image - JPG
+                        parseJexerImageFile(2, p[2], p[3]);
+                    }
                 }
-
             }
 
             // Go to SCAN_GROUND state
@@ -4814,11 +5177,19 @@ public class ECMA48 implements Runnable {
     private void pmPut(final char pmChar) {
         // System.err.println("pmPut: " + pmChar);
 
+        boolean pmEnd = false;
+
+        if ((pmChar == '\\')
+            && (collectBuffer.charAt(collectBuffer.length() - 1) == '\033')
+        ) {
+            pmEnd = true;
+        }
+
         // Collect first
         collectBuffer.append(pmChar);
 
         // Xterm cases...
-        if (collectBuffer.toString().endsWith("\033\\")) {
+        if (pmEnd) {
             String arg = null;
             arg = collectBuffer.substring(0, collectBuffer.length() - 2);
 
@@ -4905,16 +5276,12 @@ public class ECMA48 implements Runnable {
      *
      * @param ch character from the remote side
      */
-    private void consume(int ch) {
+    private void consume(final int ch) {
+        readCount++;
 
         // DEBUG
         // System.err.printf("%c STATE = %s\n", ch, scanState);
 
-        // Special case for VT10x: 7-bit characters only
-        if ((type == DeviceType.VT100) || (type == DeviceType.VT102)) {
-            ch = (ch & 0x7F);
-        }
-
         // Special "anywhere" states
 
         // 18, 1A                     --> execute, then switch to SCAN_GROUND
@@ -6687,7 +7054,7 @@ public class ECMA48 implements Runnable {
 
             // 0x71 goes to DCS_SIXEL
             if (ch == 0x71) {
-                sixelParseBuffer = new StringBuilder();
+                sixelParseBuffer.setLength(0);
                 scanState = ScanState.DCS_SIXEL;
             } else if ((ch >= 0x40) && (ch <= 0x7E)) {
                 // 0x40-7E goes to DCS_PASSTHROUGH
@@ -6772,7 +7139,7 @@ public class ECMA48 implements Runnable {
 
             // 0x71 goes to DCS_SIXEL
             if (ch == 0x71) {
-                sixelParseBuffer = new StringBuilder();
+                sixelParseBuffer.setLength(0);
                 scanState = ScanState.DCS_SIXEL;
             } else if ((ch >= 0x40) && (ch <= 0x7E)) {
                 // 0x40-7E goes to DCS_PASSTHROUGH
@@ -7036,87 +7403,19 @@ public class ECMA48 implements Runnable {
             // Sixel data was malformed in some way, bail out.
             return;
         }
-
-        /*
-         * Procedure:
-         *
-         * Break up the image into text cell sized pieces as a new array of
-         * Cells.
-         *
-         * Note original column position x0.
-         *
-         * For each cell:
-         *
-         * 1. Advance (printCharacter(' ')) for horizontal increment, or
-         *    index (linefeed() + cursorPosition(y, x0)) for vertical
-         *    increment.
-         *
-         * 2. Set (x, y) cell image data.
-         *
-         * 3. For the right and bottom edges:
-         *
-         *   a. Render the text to pixels using Terminus font.
-         *
-         *   b. Blit the image on top of the text, using alpha channel.
-         */
-        int cellColumns = image.getWidth() / textWidth;
-        if (cellColumns * textWidth < image.getWidth()) {
-            cellColumns++;
-        }
-        int cellRows = image.getHeight() / textHeight;
-        if (cellRows * textHeight < image.getHeight()) {
-            cellRows++;
-        }
-
-        // Break the image up into an array of cells.
-        Cell [][] cells = new Cell[cellColumns][cellRows];
-
-        for (int x = 0; x < cellColumns; x++) {
-            for (int y = 0; y < cellRows; y++) {
-
-                int width = textWidth;
-                if ((x + 1) * textWidth > image.getWidth()) {
-                    width = image.getWidth() - (x * textWidth);
-                }
-                int height = textHeight;
-                if ((y + 1) * textHeight > image.getHeight()) {
-                    height = image.getHeight() - (y * textHeight);
-                }
-
-                Cell cell = new Cell();
-                cell.setImage(image.getSubimage(x * textWidth,
-                        y * textHeight, width, height));
-
-                cells[x][y] = cell;
-            }
-        }
-
-        int x0 = currentState.cursorX;
-        for (int y = 0; y < cellRows; y++) {
-            for (int x = 0; x < cellColumns; x++) {
-                assert (currentState.cursorX <= rightMargin);
-
-                // TODO: Render text of current cell first, then image over
-                // it (accounting for blank pixels).  For now, just copy the
-                // cell.
-                DisplayLine line = display.get(currentState.cursorY);
-                line.replace(currentState.cursorX, cells[x][y]);
-
-                // If at the end of the visible screen, stop.
-                if (currentState.cursorX == rightMargin) {
-                    break;
-                }
-                // Room for more image on the visible screen.
-                currentState.cursorX++;
-            }
-            linefeed();
-            cursorPosition(currentState.cursorY, x0);
+        if ((image.getWidth() < 1)
+            || (image.getWidth() > 10000)
+            || (image.getHeight() < 1)
+            || (image.getHeight() > 10000)
+        ) {
+            return;
         }
 
+        imageToCells(image, true);
     }
 
     /**
-     * Parse a "Jexer" image string into a bitmap image, and overlay that
+     * Parse a "Jexer" RGB image string into a bitmap image, and overlay that
      * image onto the text cells.
      *
      * @param pw width token
@@ -7124,7 +7423,7 @@ public class ECMA48 implements Runnable {
      * @param ps scroll token
      * @param data pixel data
      */
-    private void parseJexerImage(final String pw, final String ph,
+    private void parseJexerImageRGB(final String pw, final String ph,
         final String ps, final String data) {
 
         int imageWidth = 0;
@@ -7152,8 +7451,7 @@ public class ECMA48 implements Runnable {
             return;
         }
 
-        java.util.Base64.Decoder base64 = java.util.Base64.getDecoder();
-        byte [] bytes = base64.decode(data);
+        byte [] bytes = StringUtils.fromBase64(data.getBytes());
         if (bytes.length != (imageWidth * imageHeight * 3)) {
             return;
         }
@@ -7180,6 +7478,93 @@ public class ECMA48 implements Runnable {
             }
         }
 
+        imageToCells(image, scroll);
+    }
+
+    /**
+     * Parse a "Jexer" PNG or JPG image string into a bitmap image, and
+     * overlay that image onto the text cells.
+     *
+     * @param type 1 for PNG, 2 for JPG
+     * @param ps scroll token
+     * @param data pixel data
+     */
+    private void parseJexerImageFile(final int type, final String ps,
+        final String data) {
+
+        int imageWidth = 0;
+        int imageHeight = 0;
+        boolean scroll = false;
+        BufferedImage image = null;
+        try {
+            byte [] bytes = StringUtils.fromBase64(data.getBytes());
+
+            switch (type) {
+            case 1:
+                if ((bytes[0] != (byte) 0x89)
+                    || (bytes[1] != 'P')
+                    || (bytes[2] != 'N')
+                    || (bytes[3] != 'G')
+                    || (bytes[4] != (byte) 0x0D)
+                    || (bytes[5] != (byte) 0x0A)
+                    || (bytes[6] != (byte) 0x1A)
+                    || (bytes[7] != (byte) 0x0A)
+                ) {
+                    // File does not have PNG header, bail out.
+                    return;
+                }
+                break;
+
+            case 2:
+                if ((bytes[0] != (byte) 0XFF)
+                    || (bytes[1] != (byte) 0xD8)
+                    || (bytes[2] != (byte) 0xFF)
+                ) {
+                    // File does not have JPG header, bail out.
+                    return;
+                }
+                break;
+
+            default:
+                // Unsupported type, bail out.
+                return;
+            }
+
+            image = ImageIO.read(new ByteArrayInputStream(bytes));
+        } catch (IOException e) {
+            // SQUASH
+            return;
+        }
+        assert (image != null);
+        imageWidth = image.getWidth();
+        imageHeight = image.getHeight();
+        if ((imageWidth < 1)
+            || (imageWidth > 10000)
+            || (imageHeight < 1)
+            || (imageHeight > 10000)
+        ) {
+            return;
+        }
+        if (ps.equals("1")) {
+            scroll = true;
+        } else if (ps.equals("0")) {
+            scroll = false;
+        } else {
+            return;
+        }
+
+        imageToCells(image, scroll);
+    }
+
+    /**
+     * Break up an image into the cells at the current cursor.
+     *
+     * @param image the image to display
+     * @param scroll if true, scroll the image and move the cursor
+     */
+    private void imageToCells(final BufferedImage image, final boolean scroll) {
+        assert (image != null);
+
         /*
          * Procedure:
          *
@@ -7227,19 +7612,39 @@ public class ECMA48 implements Runnable {
                 }
 
                 Cell cell = new Cell();
-                cell.setImage(image.getSubimage(x * textWidth,
-                        y * textHeight, width, height));
+                if ((width != textWidth) || (height != textHeight)) {
+                    BufferedImage newImage;
+                    newImage = new BufferedImage(textWidth, textHeight,
+                        BufferedImage.TYPE_INT_ARGB);
+
+                    Graphics gr = newImage.getGraphics();
+                    gr.drawImage(image.getSubimage(x * textWidth,
+                            y * textHeight, width, height),
+                        0, 0, null, null);
+                    gr.dispose();
+                    cell.setImage(newImage);
+                } else {
+                    cell.setImage(image.getSubimage(x * textWidth,
+                            y * textHeight, width, height));
+                }
 
                 cells[x][y] = cell;
             }
         }
 
         int x0 = currentState.cursorX;
+        int y0 = currentState.cursorY;
         for (int y = 0; y < cellRows; y++) {
             for (int x = 0; x < cellColumns; x++) {
                 assert (currentState.cursorX <= rightMargin);
+
+                // A real sixel terminal would render the text of the current
+                // cell first, then image over it (accounting for blank
+                // pixels).  We do not support that.  A cell is either text,
+                // or image, but not a mix of image-over-text.
                 DisplayLine line = display.get(currentState.cursorY);
                 line.replace(currentState.cursorX, cells[x][y]);
+
                 // If at the end of the visible screen, stop.
                 if (currentState.cursorX == rightMargin) {
                     break;
@@ -7247,15 +7652,24 @@ public class ECMA48 implements Runnable {
                 // Room for more image on the visible screen.
                 currentState.cursorX++;
             }
-            if ((scroll == true)
-                || ((scroll == false)
-                    && (currentState.cursorY < scrollRegionBottom))
-            ) {
+            if (currentState.cursorY < scrollRegionBottom - 1) {
+                // Not at the bottom, down a line.
                 linefeed();
+            } else if (scroll == true) {
+                // At the bottom, scroll as needed.
+                linefeed();
+            } else {
+                // At the bottom, no more scrolling, done.
+                break;
             }
+
             cursorPosition(currentState.cursorY, x0);
         }
 
+        if (scroll == false) {
+            cursorPosition(y0, x0);
+        }
+
     }
 
 }
similarity index 99%
rename from src/jexer/tterminal/Sixel.java
rename to tterminal/Sixel.java
index a4c00fc67f1da1ca38847b9bb5dad63daac5e006..b91e77a98beebc8cdfc726426654fd07c1b43967 100644 (file)
@@ -31,7 +31,6 @@ package jexer.tterminal;
 import java.awt.Color;
 import java.awt.Graphics2D;
 import java.awt.image.BufferedImage;
-import java.util.ArrayList;
 import java.util.HashMap;
 
 /**
@@ -574,10 +573,10 @@ public class Sixel {
         case REPEAT:
             if ((ch >= '0') && (ch <= '9')) {
                 if (repeatCount == -1) {
-                    repeatCount = (int) (ch - '0');
+                    repeatCount = (ch - '0');
                 } else {
                     repeatCount *= 10;
-                    repeatCount += (int) (ch - '0');
+                    repeatCount += (ch - '0');
                 }
             }
             return;
similarity index 92%
rename from src/jexer/ttree/TTreeViewWidget.java
rename to ttree/TTreeViewWidget.java
index 080a200497dfbe5389a8f62f3593688eb325fb72..13beac321bac9e46b0f81af48ac069efe3f5c890 100644 (file)
@@ -268,11 +268,55 @@ public class TTreeViewWidget extends TScrollableWidget {
     // TScrollableWidget ------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Override TWidget's width: we need to set child widget widths.
+     *
+     * @param width new widget width
+     */
+    @Override
+    public void setWidth(final int width) {
+        super.setWidth(width);
+        if (hScroller != null) {
+            hScroller.setWidth(getWidth() - 1);
+        }
+        if (vScroller != null) {
+            vScroller.setX(getWidth() - 1);
+        }
+        if (treeView != null) {
+            treeView.setWidth(getWidth() - 1);
+        }
+        reflowData();
+    }
+
+    /**
+     * Override TWidget's height: we need to set child widget heights.
+     *
+     * @param height new widget height
+     */
+    @Override
+    public void setHeight(final int height) {
+        super.setHeight(height);
+        if (hScroller != null) {
+            hScroller.setY(getHeight() - 1);
+        }
+        if (vScroller != null) {
+            vScroller.setHeight(getHeight() - 1);
+        }
+        if (treeView != null) {
+            treeView.setHeight(getHeight() - 1);
+        }
+        reflowData();
+    }
+
     /**
      * Resize text and scrollbars for a new width/height.
      */
     @Override
     public void reflowData() {
+        if (treeView == null) {
+            return;
+        }
+
         int selectedRow = 0;
         boolean foundSelectedRow = false;