package jexer;
import java.io.File;
+import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
import jexer.bits.ColorTheme;
import jexer.bits.StringUtils;
import jexer.event.TCommandEvent;
import jexer.backend.SwingBackend;
import jexer.backend.ECMA48Backend;
import jexer.backend.TWindowBackend;
+import jexer.help.HelpFile;
+import jexer.help.Topic;
import jexer.menu.TMenu;
import jexer.menu.TMenuItem;
import jexer.menu.TSubMenu;
*/
private Backend backend;
+ /**
+ * The clipboard for copy and paste.
+ */
+ private Clipboard clipboard = new Clipboard();
+
/**
* Actual mouse coordinate X.
*/
*/
private int mouseY;
- /**
- * Old version of mouse coordinate X.
- */
- private int oldMouseX;
-
- /**
- * Old version mouse coordinate Y.
- */
- private int oldMouseY;
-
/**
* Old drawn version of mouse coordinate X.
*/
*/
private List<TWindow> windows;
- /**
- * The currently acive window.
- */
- private TWindow activeWindow = null;
-
/**
* Timers that are being ticked.
*/
*/
private long screenResizeTime = 0;
+ /**
+ * If true, screen selection is a rectangle.
+ */
+ private boolean screenSelectionRectangle = false;
+
+ /**
+ * If true, the mouse is dragging a screen selection.
+ */
+ private boolean inScreenSelection = false;
+
+ /**
+ * Screen selection starting X.
+ */
+ private int screenSelectionX0;
+
+ /**
+ * Screen selection starting Y.
+ */
+ private int screenSelectionY0;
+
+ /**
+ * Screen selection ending X.
+ */
+ private int screenSelectionX1;
+
+ /**
+ * Screen selection ending Y.
+ */
+ private int screenSelectionY1;
+
+ /**
+ * The help file data. Note package private access.
+ */
+ HelpFile helpFile;
+
+ /**
+ * The stack of help topics. Note package private access.
+ */
+ ArrayList<Topic> helpTopics = new ArrayList<Topic>();
+
/**
* WidgetEventHandler is the main event consumer loop. There are at most
* two such threads in existence: the primary for normal case and a
}
}
+ // Load the help system
+ invokeLater(new Runnable() {
+ /*
+ * This isn't the best solution. But basically if a TApplication
+ * subclass constructor throws and needs to use TExceptionDialog,
+ * it may end up at the bottom of the window stack with a bunch
+ * of modal windows on top of it if said constructors spawn their
+ * windows also via invokeLater(). But if they don't do that,
+ * and instead just conventionally construct their windows, then
+ * this exception dialog will end up on top where it should be.
+ */
+ public void run() {
+ try {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ helpFile = new HelpFile();
+ helpFile.load(loader.getResourceAsStream("help.xml"));
+ } catch (Exception e) {
+ new TExceptionDialog(TApplication.this, e);
+ }
+ }
+ });
}
// ------------------------------------------------------------------------
return true;
}
+ if (command.equals(cmHelp)) {
+ if (getActiveWindow() != null) {
+ new THelpWindow(this, getActiveWindow().getHelpTopic());
+ } else {
+ new THelpWindow(this);
+ }
+ return true;
+ }
+
if (command.equals(cmShell)) {
openTerminal(0, 0, TWindow.RESIZABLE);
return true;
return true;
}
+ if (menu.getId() == TMenu.MID_HELP_HELP) {
+ new THelpWindow(this, THelpWindow.HELP_HELP);
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_HELP_CONTENTS) {
+ new THelpWindow(this, helpFile.getTableOfContents());
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_HELP_INDEX) {
+ new THelpWindow(this, helpFile.getIndex());
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_HELP_SEARCH) {
+ TInputBox inputBox = inputBox(i18n.
+ getString("searchHelpInputBoxTitle"),
+ i18n.getString("searchHelpInputBoxCaption"), "",
+ TInputBox.Type.OKCANCEL);
+ if (inputBox.isOk()) {
+ new THelpWindow(this,
+ helpFile.getSearchResults(inputBox.getText()));
+ }
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_HELP_PREVIOUS) {
+ if (helpTopics.size() > 1) {
+ Topic previous = helpTopics.remove(helpTopics.size() - 2);
+ helpTopics.remove(helpTopics.size() - 1);
+ new THelpWindow(this, previous);
+ } else {
+ new THelpWindow(this, helpFile.getTableOfContents());
+ }
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_HELP_ACTIVE_FILE) {
+ try {
+ List<String> filters = new ArrayList<String>();
+ filters.add("^.*\\.[Xx][Mm][Ll]$");
+ String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN,
+ filters);
+ if (filename != null) {
+ helpTopics = new ArrayList<Topic>();
+ helpFile = new HelpFile();
+ helpFile.load(new FileInputStream(filename));
+ }
+ } catch (Exception e) {
+ // Show this exception to the user.
+ new TExceptionDialog(this, e);
+ }
+ return true;
+ }
+
if (menu.getId() == TMenu.MID_SHELL) {
openTerminal(0, 0, TWindow.RESIZABLE);
return true;
new TFontChooserWindow(this);
return true;
}
+
+ if (menu.getId() == TMenu.MID_CUT) {
+ postMenuEvent(new TCommandEvent(cmCut));
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_COPY) {
+ postMenuEvent(new TCommandEvent(cmCopy));
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_PASTE) {
+ postMenuEvent(new TCommandEvent(cmPaste));
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_CLEAR) {
+ postMenuEvent(new TCommandEvent(cmClear));
+ return true;
+ }
+
return false;
}
Thread.currentThread() + " finishEventProcessing()\n");
}
+ // See if we need to enable/disable the edit menu.
+ EditMenuUser widget = null;
+ if (activeMenu == null) {
+ TWindow activeWindow = getActiveWindow();
+ if (activeWindow != null) {
+ if (activeWindow.getActiveChild() instanceof EditMenuUser) {
+ widget = (EditMenuUser) activeWindow.getActiveChild();
+ }
+ } else if (desktop != null) {
+ if (desktop.getActiveChild() instanceof EditMenuUser) {
+ widget = (EditMenuUser) desktop.getActiveChild();
+ }
+ }
+ if (widget == null) {
+ disableMenuItem(TMenu.MID_CUT);
+ disableMenuItem(TMenu.MID_COPY);
+ disableMenuItem(TMenu.MID_PASTE);
+ disableMenuItem(TMenu.MID_CLEAR);
+ } else {
+ if (widget.isEditMenuCut()) {
+ enableMenuItem(TMenu.MID_CUT);
+ } else {
+ disableMenuItem(TMenu.MID_CUT);
+ }
+ if (widget.isEditMenuCopy()) {
+ enableMenuItem(TMenu.MID_COPY);
+ } else {
+ disableMenuItem(TMenu.MID_COPY);
+ }
+ if (widget.isEditMenuPaste()) {
+ enableMenuItem(TMenu.MID_PASTE);
+ } else {
+ disableMenuItem(TMenu.MID_PASTE);
+ }
+ if (widget.isEditMenuClear()) {
+ enableMenuItem(TMenu.MID_CLEAR);
+ } else {
+ disableMenuItem(TMenu.MID_CLEAR);
+ }
+ }
+ }
+
// Process timers and call doIdle()'s
doIdle();
}
mouseX = 0;
mouseY = 0;
- oldMouseX = 0;
- oldMouseY = 0;
}
if (desktop != null) {
desktop.setDimensions(0, desktopTop, resize.getWidth(),
typingHidMouse = false;
TMouseEvent mouse = (TMouseEvent) event;
+ if (mouse.isMouse1() && (mouse.isShift() || mouse.isCtrl())) {
+ // Screen selection.
+ if (inScreenSelection) {
+ screenSelectionX1 = mouse.getX();
+ screenSelectionY1 = mouse.getY();
+ } else {
+ inScreenSelection = true;
+ screenSelectionX0 = mouse.getX();
+ screenSelectionY0 = mouse.getY();
+ screenSelectionX1 = mouse.getX();
+ screenSelectionY1 = mouse.getY();
+ screenSelectionRectangle = mouse.isCtrl();
+ }
+ } else {
+ if (inScreenSelection) {
+ getScreen().copySelection(clipboard, screenSelectionX0,
+ screenSelectionY0, screenSelectionX1, screenSelectionY1,
+ screenSelectionRectangle);
+ }
+ inScreenSelection = false;
+ }
+
if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
- oldMouseX = mouseX;
- oldMouseY = mouseY;
mouseX = mouse.getX();
mouseY = mouse.getY();
} else {
mouse.getAbsoluteX(), mouse.getAbsoluteY(),
mouse.isMouse1(), mouse.isMouse2(),
mouse.isMouse3(),
- mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+ mouse.isMouseWheelUp(), mouse.isMouseWheelDown(),
+ mouse.isAlt(), mouse.isCtrl(), mouse.isShift());
} else {
// The first click of a potential double-click.
// shortcutted by the active window, and if so dispatch the menu
// event.
boolean windowWillShortcut = false;
+ TWindow activeWindow = getActiveWindow();
if (activeWindow != null) {
assert (activeWindow.isShown());
if (activeWindow.isShortcutKeypress(keypress.getKey())) {
// Dispatch events to the active window -------------------------------
boolean dispatchToDesktop = true;
- TWindow window = activeWindow;
+ TWindow window = getActiveWindow();
if (window != null) {
assert (window.isActive());
assert (window.isShown());
TMouseEvent mouse = (TMouseEvent) event;
if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
- oldMouseX = mouseX;
- oldMouseY = mouseY;
mouseX = mouse.getX();
mouseY = mouse.getY();
} else {
mouse.getAbsoluteX(), mouse.getAbsoluteY(),
mouse.isMouse1(), mouse.isMouse2(),
mouse.isMouse3(),
- mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+ mouse.isMouseWheelUp(), mouse.isMouseWheelDown(),
+ mouse.isAlt(), mouse.isCtrl(), mouse.isShift());
} else {
// The first click of a potential double-click.
desktop.onIdle();
}
- // Run any invokeLaters
+ // Run any invokeLaters. We make a copy, and run that, because one
+ // of these Runnables might add call TApplication.invokeLater().
+ List<Runnable> invokes = new ArrayList<Runnable>();
synchronized (invokeLaters) {
- for (Runnable invoke: invokeLaters) {
- invoke.run();
- }
+ invokes.addAll(invokeLaters);
invokeLaters.clear();
}
+ for (Runnable invoke: invokes) {
+ invoke.run();
+ }
+ doRepaint();
}
return theme;
}
+ /**
+ * Get the clipboard.
+ *
+ * @return the clipboard
+ */
+ public final Clipboard getClipboard() {
+ return clipboard;
+ }
+
/**
* Repaint the screen on the next update.
*/
* @return the active window, or null if it is not set
*/
public final TWindow getActiveWindow() {
- return activeWindow;
+ for (TWindow window: windows) {
+ if (window.isShown() && window.isActive()) {
+ return window;
+ }
+ }
+ return null;
}
/**
String version = getClass().getPackage().getImplementationVersion();
if (version == null) {
// This is Java 9+, use a hardcoded string here.
- version = "0.3.2";
+ version = "1.0.0";
}
messageBox(i18n.getString("aboutDialogTitle"),
MessageFormat.format(i18n.getString("aboutDialogText"), version),
// ------------------------------------------------------------------------
/**
- * Invert the cell color at a position. This is used to track the mouse.
- *
- * @param x column position
- * @param y row position
- */
- private void invertCell(final int x, final int y) {
- invertCell(x, y, false);
- }
-
- /**
- * Invert the cell color at a position. This is used to track the mouse.
+ * Draw the text mouse at position.
*
* @param x column position
* @param y row position
- * @param onlyThisCell if true, only invert this cell
*/
- private void invertCell(final int x, final int y,
- final boolean onlyThisCell) {
+ private void drawTextMouse(final int x, final int y) {
+ TWindow activeWindow = getActiveWindow();
if (debugThreads) {
- System.err.printf("%d %s invertCell() %d %d\n",
+ System.err.printf("%d %s drawTextMouse() %d %d\n",
System.currentTimeMillis(), Thread.currentThread(), x, y);
if (activeWindow != null) {
}
}
- Cell cell = getScreen().getCharXY(x, y);
- if (cell.isImage()) {
- cell.invertImage();
- }
- if (cell.getForeColorRGB() < 0) {
- cell.setForeColor(cell.getForeColor().invert());
- } else {
- cell.setForeColorRGB(cell.getForeColorRGB() ^ 0x00ffffff);
- }
- if (cell.getBackColorRGB() < 0) {
- cell.setBackColor(cell.getBackColor().invert());
- } else {
- cell.setBackColorRGB(cell.getBackColorRGB() ^ 0x00ffffff);
- }
- getScreen().putCharXY(x, y, cell);
- if ((onlyThisCell == true) || (cell.getWidth() == Cell.Width.SINGLE)) {
- return;
- }
-
- // This cell is one half of a fullwidth glyph. Invert the other
- // half.
- if (cell.getWidth() == Cell.Width.LEFT) {
- if (x < getScreen().getWidth() - 1) {
- Cell rightHalf = getScreen().getCharXY(x + 1, y);
- if (rightHalf.getWidth() == Cell.Width.RIGHT) {
- invertCell(x + 1, y, true);
- return;
- }
- }
- }
- if (cell.getWidth() == Cell.Width.RIGHT) {
- if (x > 0) {
- Cell leftHalf = getScreen().getCharXY(x - 1, y);
- if (leftHalf.getWidth() == Cell.Width.LEFT) {
- invertCell(x - 1, y, true);
- }
- }
- }
+ getScreen().invertCell(x, y);
}
/**
}
}
+ if (inScreenSelection) {
+ getScreen().setSelection(screenSelectionX0,
+ screenSelectionY0, screenSelectionX1, screenSelectionY1,
+ screenSelectionRectangle);
+ }
+
if ((textMouse == true) && (typingHidMouse == false)) {
// Draw mouse at the new position.
- invertCell(mouseX, mouseY);
+ drawTextMouse(mouseX, mouseY);
}
oldDrawnMouseX = mouseX;
// Draw the status bar of the top-level window
TStatusBar statusBar = null;
if (topLevel != null) {
- statusBar = topLevel.getStatusBar();
+ if (topLevel.isShown()) {
+ statusBar = topLevel.getStatusBar();
+ }
}
if (statusBar != null) {
getScreen().resetClipping();
getScreen().unsetImageRow(mouseY);
}
}
+
+ if (inScreenSelection) {
+ getScreen().setSelection(screenSelectionX0, screenSelectionY0,
+ screenSelectionX1, screenSelectionY1, screenSelectionRectangle);
+ }
+
if ((textMouse == true) && (typingHidMouse == false)) {
- invertCell(mouseX, mouseY);
+ drawTextMouse(mouseX, mouseY);
}
oldDrawnMouseX = mouseX;
oldDrawnMouseY = mouseY;
*
* @param window the window to become the new active window
*/
- public void activateWindow(final TWindow window) {
+ public final void activateWindow(final TWindow window) {
if (hasWindow(window) == false) {
/*
* Someone has a handle to a window I don't have. Ignore this
return;
}
- // Whatever window might be moving/dragging, stop it now.
- for (TWindow w: windows) {
- if (w.inMovements()) {
- w.stopMovements();
- }
+ if (modalWindowActive() && !window.isModal()) {
+ // Do not activate a non-modal on top of a modal.
+ return;
}
- assert (windows.size() > 0);
+ synchronized (windows) {
+ // Whatever window might be moving/dragging, stop it now.
+ for (TWindow w: windows) {
+ if (w.inMovements()) {
+ w.stopMovements();
+ }
+ }
- if (window.isHidden()) {
- // Unhiding will also activate.
- showWindow(window);
- return;
- }
- assert (window.isShown());
+ assert (windows.size() > 0);
- if (windows.size() == 1) {
- assert (window == windows.get(0));
- if (activeWindow == null) {
- activeWindow = window;
- window.setZ(0);
- activeWindow.setActive(true);
- activeWindow.onFocus();
+ if (window.isHidden()) {
+ // Unhiding will also activate.
+ showWindow(window);
+ return;
}
+ assert (window.isShown());
- assert (window.isActive());
- assert (activeWindow == window);
- return;
- }
+ if (windows.size() == 1) {
+ assert (window == windows.get(0));
+ window.setZ(0);
+ window.setActive(true);
+ window.onFocus();
+ return;
+ }
- if (activeWindow == window) {
- assert (window.isActive());
+ if (getActiveWindow() == window) {
+ assert (window.isActive());
- // Window is already active, do nothing.
- return;
- }
+ // Window is already active, do nothing.
+ return;
+ }
- assert (!window.isActive());
- if (activeWindow != null) {
- activeWindow.setActive(false);
+ assert (!window.isActive());
- // Increment every window Z that is on top of window
+ window.setZ(-1);
+ Collections.sort(windows);
+ int newZ = 0;
for (TWindow w: windows) {
- if (w == window) {
- continue;
- }
- if (w.getZ() < window.getZ()) {
- w.setZ(w.getZ() + 1);
+ w.setZ(newZ);
+ newZ++;
+ if ((w != window) && w.isActive()) {
+ w.onUnfocus();
}
+ w.setActive(false);
}
+ window.setActive(true);
+ window.onFocus();
+
+ } // synchronized (windows)
- // Unset activeWindow now before unfocus, so that a window
- // lifecycle change inside onUnfocus() doesn't call
- // switchWindow() and lead to a stack overflow.
- TWindow oldActiveWindow = activeWindow;
- activeWindow = null;
- oldActiveWindow.onUnfocus();
- }
- activeWindow = window;
- activeWindow.setZ(0);
- activeWindow.setActive(true);
- activeWindow.onFocus();
return;
}
return;
}
- // Whatever window might be moving/dragging, stop it now.
- for (TWindow w: windows) {
- if (w.inMovements()) {
- w.stopMovements();
+ synchronized (windows) {
+
+ // Whatever window might be moving/dragging, stop it now.
+ for (TWindow w: windows) {
+ if (w.inMovements()) {
+ w.stopMovements();
+ }
}
- }
- assert (windows.size() > 0);
+ assert (windows.size() > 0);
- if (!window.hidden) {
- if (window == activeWindow) {
- if (shownWindowCount() > 1) {
- switchWindow(true);
- } else {
- activeWindow = null;
- window.setActive(false);
- window.onUnfocus();
- }
+ if (window.hidden) {
+ return;
}
+
+ window.setActive(false);
window.hidden = true;
window.onHide();
- }
+
+ TWindow activeWindow = null;
+ for (TWindow w: windows) {
+ if (w.isShown()) {
+ activeWindow = w;
+ break;
+ }
+ }
+ assert (activeWindow != window);
+ if (activeWindow != null) {
+ activateWindow(activeWindow);
+ }
+
+ } // synchronized (windows)
+
}
/**
return;
}
- // Whatever window might be moving/dragging, stop it now.
- for (TWindow w: windows) {
- if (w.inMovements()) {
- w.stopMovements();
- }
- }
-
- assert (windows.size() > 0);
-
if (window.hidden) {
window.hidden = false;
window.onShow();
activateWindow(window);
}
+
}
/**
- * Close window. Note that the window's destructor is NOT called by this
- * method, instead the GC is assumed to do the cleanup.
+ * Close window.
*
* @param window the window to remove
*/
window.onPreClose();
synchronized (windows) {
- // Whatever window might be moving/dragging, stop it now.
- for (TWindow w: windows) {
- if (w.inMovements()) {
- w.stopMovements();
- }
- }
- int z = window.getZ();
- window.setZ(-1);
+ window.stopMovements();
window.onUnfocus();
windows.remove(window);
Collections.sort(windows);
- activeWindow = null;
- int newZ = 0;
- boolean foundNextWindow = false;
+ TWindow nextWindow = null;
+ int newZ = 0;
for (TWindow w: windows) {
+ w.stopMovements();
w.setZ(newZ);
newZ++;
if (w.isHidden()) {
continue;
}
-
- if (foundNextWindow == false) {
- foundNextWindow = true;
- w.setActive(true);
- w.onFocus();
- assert (activeWindow == null);
- activeWindow = w;
- continue;
+ if (nextWindow == null) {
+ nextWindow = w;
+ } else {
+ if (w.isActive()) {
+ w.setActive(false);
+ w.onUnfocus();
+ }
}
+ }
- if (w.isActive()) {
- w.setActive(false);
- w.onUnfocus();
- }
+ if (nextWindow != null) {
+ nextWindow.setActive(true);
+ nextWindow.onFocus();
}
- }
+
+ } // synchronized (windows)
// Perform window cleanup
window.onClose();
synchronized (secondaryEventHandler) {
secondaryEventHandler.notify();
}
- }
+
+ } // synchronized (windows)
// Permit desktop to be active if it is the only thing left.
if (desktop != null) {
if (shownWindowCount() < 2) {
return;
}
- assert (activeWindow != null);
+
+ if (modalWindowActive()) {
+ // Do not switch if a window is modal
+ return;
+ }
synchronized (windows) {
- // Whatever window might be moving/dragging, stop it now.
- for (TWindow w: windows) {
- if (w.inMovements()) {
- w.stopMovements();
- }
- }
- // Swap z/active between active window and the next in the list
- int activeWindowI = -1;
- for (int i = 0; i < windows.size(); i++) {
- if (windows.get(i) == activeWindow) {
- assert (activeWindow.isActive());
- activeWindowI = i;
- break;
+ TWindow window = windows.get(0);
+ do {
+ assert (window != null);
+ if (forward) {
+ window.setZ(windows.size());
} else {
- assert (!windows.get(0).isActive());
+ TWindow lastWindow = windows.get(windows.size() - 1);
+ lastWindow.setZ(-1);
}
- }
- assert (activeWindowI >= 0);
-
- // Do not switch if a window is modal
- if (activeWindow.isModal()) {
- return;
- }
- int nextWindowI = activeWindowI;
- for (;;) {
- if (forward) {
- nextWindowI++;
- nextWindowI %= windows.size();
- } else {
- nextWindowI--;
- if (nextWindowI < 0) {
- nextWindowI = windows.size() - 1;
- }
+ Collections.sort(windows);
+ int newZ = 0;
+ for (TWindow w: windows) {
+ w.setZ(newZ);
+ newZ++;
}
- if (windows.get(nextWindowI).isShown()) {
- activateWindow(windows.get(nextWindowI));
- break;
+ window = windows.get(0);
+ } while (!window.isShown());
+
+ // The next visible window is now on top. Renumber the list.
+ for (TWindow w: windows) {
+ w.stopMovements();
+ if ((w != window) && w.isActive()) {
+ assert (w.isShown());
+ w.setActive(false);
+ w.onUnfocus();
}
}
- } // synchronized (windows)
+ // Next visible window is on top.
+ assert (window.isShown());
+ window.setActive(true);
+ window.onFocus();
+
+ } // synchronized (windows)
}
/**
}
w.setZ(w.getZ() + 1);
}
- }
- windows.add(window);
- if (window.isShown()) {
- activeWindow = window;
- activeWindow.setZ(0);
- activeWindow.setActive(true);
- activeWindow.onFocus();
+ window.setZ(0);
+ window.setActive(true);
+ window.onFocus();
+ windows.add(0, window);
+ } else {
+ window.setZ(windows.size());
+ windows.add(window);
}
if (((window.flags & TWindow.CENTERED) == 0)
if (desktop != null) {
desktop.setActive(false);
}
+
}
/**
* @return true if the active window is overriding the menu
*/
private boolean overrideMenuWindowActive() {
+ TWindow activeWindow = getActiveWindow();
if (activeWindow != null) {
if (activeWindow.hasOverriddenMenu()) {
return true;
|| (mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
) {
synchronized (windows) {
- Collections.sort(windows);
if (windows.get(0).isModal()) {
// Modal windows don't switch
return;
}
if (window.mouseWouldHit(mouse)) {
- if (window == windows.get(0)) {
- // Clicked on the same window, nothing to do
- assert (window.isActive());
- return;
- }
-
- // We will be switching to another window
- assert (windows.get(0).isActive());
- assert (windows.get(0) == activeWindow);
- assert (!window.isActive());
- if (activeWindow != null) {
- activeWindow.onUnfocus();
- activeWindow.setActive(false);
- activeWindow.setZ(window.getZ());
- }
- activeWindow = window;
- window.setZ(0);
- window.setActive(true);
- window.onFocus();
+ activateWindow(window);
return;
}
}
*/
public final TMenu addEditMenu() {
TMenu editMenu = addMenu(i18n.getString("editMenuTitle"));
- editMenu.addDefaultItem(TMenu.MID_CUT);
- editMenu.addDefaultItem(TMenu.MID_COPY);
- editMenu.addDefaultItem(TMenu.MID_PASTE);
- editMenu.addDefaultItem(TMenu.MID_CLEAR);
+ editMenu.addDefaultItem(TMenu.MID_UNDO, false);
+ editMenu.addDefaultItem(TMenu.MID_REDO, false);
+ editMenu.addSeparator();
+ editMenu.addDefaultItem(TMenu.MID_CUT, false);
+ editMenu.addDefaultItem(TMenu.MID_COPY, false);
+ editMenu.addDefaultItem(TMenu.MID_PASTE, false);
+ editMenu.addDefaultItem(TMenu.MID_CLEAR, false);
TStatusBar statusBar = editMenu.newStatusBar(i18n.
getString("editMenuStatus"));
statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
aboutDialogTitle=About
aboutDialogText=Jexer Version {0}
+
+searchHelpInputBoxTitle=Search Help Topics
+searchHelpInputBoxCaption=Search help topics for (regex):
package jexer;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
import jexer.bits.CellAttributes;
+import jexer.bits.StringUtils;
+import jexer.event.TCommandEvent;
import jexer.event.TKeypressEvent;
import jexer.event.TMouseEvent;
import jexer.event.TResizeEvent;
import jexer.teditor.Document;
import jexer.teditor.Line;
import jexer.teditor.Word;
+import static jexer.TCommand.*;
import static jexer.TKeypress.*;
/**
* TEditorWidget displays an editable text document. It is unaware of
* scrolling behavior, but can respond to mouse and keyboard events.
*/
-public class TEditorWidget extends TWidget {
+public class TEditorWidget extends TWidget implements EditMenuUser {
// ------------------------------------------------------------------------
// Constants --------------------------------------------------------------
/**
* The document being edited.
*/
- private Document document;
+ protected Document document;
/**
- * The default color for the TEditor class.
+ * The default color for the editable text.
*/
private CellAttributes defaultColor = null;
*/
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 -----------------------------------------------------------
// ------------------------------------------------------------------------
}
// ------------------------------------------------------------------------
- // TWidget ----------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
// ------------------------------------------------------------------------
- /**
- * Draw the text box.
- */
- @Override
- public void draw() {
- for (int i = 0; i < getHeight(); i++) {
- // Background line
- getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor);
-
- // Now draw document's line
- if (topLine + i < document.getLineCount()) {
- Line line = document.getLine(topLine + i);
- int x = 0;
- for (Word word: line.getWords()) {
- // For now, we are cheating: draw outside the left region
- // if needed and let screen do the clipping.
- getScreen().putStringXY(x - leftColumn, i, word.getText(),
- word.getColor());
- x += word.getDisplayLength();
- if (x - leftColumn > getWidth()) {
- break;
- }
- }
- }
- }
- }
-
/**
* Handle mouse press events.
*
}
if (mouse.isMouse1()) {
- // Set the row and column
+ // Selection.
int newLine = topLine + mouse.getY();
int newX = leftColumn + mouse.getX();
+
+ inSelection = true;
+ if (newLine > document.getLineCount() - 1) {
+ selectionLine0 = document.getLineCount() - 1;
+ } else {
+ selectionLine0 = topLine + mouse.getY();
+ }
+ selectionColumn0 = leftColumn + mouse.getX();
+ selectionColumn0 = Math.max(0, Math.min(selectionColumn0,
+ document.getLine(selectionLine0).getDisplayLength() - 1));
+ selectionColumn1 = selectionColumn0;
+ selectionLine1 = selectionLine0;
+
+ // Set the row and column
if (newLine > document.getLineCount() - 1) {
// Go to the end
document.setLineNumber(document.getLineCount() - 1);
setCursorY(mouse.getY());
}
alignCursor();
+ if (inSelection) {
+ selectionColumn1 = document.getCursor();
+ selectionLine1 = document.getLineNumber();
+ }
return;
}
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;
}
*/
@Override
public void onKeypress(final TKeypressEvent keypress) {
- if (keypress.equals(kbLeft)) {
+ if (keypress.getKey().isShift()) {
+ if (keypress.equals(kbShiftLeft)
+ || keypress.equals(kbShiftRight)
+ || keypress.equals(kbShiftUp)
+ || keypress.equals(kbShiftDown)
+ || keypress.equals(kbShiftPgDn)
+ || keypress.equals(kbShiftPgUp)
+ || keypress.equals(kbShiftHome)
+ || keypress.equals(kbShiftEnd)
+ ) {
+ // Shifted navigation keys enable selection
+ if (!inSelection) {
+ inSelection = true;
+ selectionColumn0 = document.getCursor();
+ selectionLine0 = document.getLineNumber();
+ selectionColumn1 = selectionColumn0;
+ selectionLine1 = selectionLine0;
+ }
+ }
+ } else {
+ if (keypress.equals(kbLeft)
+ || keypress.equals(kbRight)
+ || keypress.equals(kbUp)
+ || keypress.equals(kbDown)
+ || keypress.equals(kbPgDn)
+ || keypress.equals(kbPgUp)
+ || keypress.equals(kbHome)
+ || keypress.equals(kbEnd)
+ ) {
+ // Non-shifted navigation keys disable selection.
+ inSelection = false;
+ }
+ if ((selectionColumn0 == selectionColumn1)
+ && (selectionLine0 == selectionLine1)
+ ) {
+ // The user clicked a spot and started typing.
+ inSelection = false;
+ }
+ }
+
+ if (keypress.equals(kbLeft)
+ || keypress.equals(kbShiftLeft)
+ ) {
document.left();
alignTopLine(false);
- } else if (keypress.equals(kbRight)) {
+ } else if (keypress.equals(kbRight)
+ || keypress.equals(kbShiftRight)
+ ) {
document.right();
alignTopLine(true);
} else if (keypress.equals(kbAltLeft)
|| keypress.equals(kbCtrlLeft)
+ || keypress.equals(kbAltShiftLeft)
+ || keypress.equals(kbCtrlShiftLeft)
) {
document.backwardsWord();
alignTopLine(false);
} else if (keypress.equals(kbAltRight)
|| keypress.equals(kbCtrlRight)
+ || keypress.equals(kbAltShiftRight)
+ || keypress.equals(kbCtrlShiftRight)
) {
document.forwardsWord();
alignTopLine(true);
- } else if (keypress.equals(kbUp)) {
+ } else if (keypress.equals(kbUp)
+ || keypress.equals(kbShiftUp)
+ ) {
document.up();
alignTopLine(false);
- } else if (keypress.equals(kbDown)) {
+ } else if (keypress.equals(kbDown)
+ || keypress.equals(kbShiftDown)
+ ) {
document.down();
alignTopLine(true);
- } else if (keypress.equals(kbPgUp)) {
+ } else if (keypress.equals(kbPgUp)
+ || keypress.equals(kbShiftPgUp)
+ ) {
document.up(getHeight() - 1);
alignTopLine(false);
- } else if (keypress.equals(kbPgDn)) {
+ } else if (keypress.equals(kbPgDn)
+ || keypress.equals(kbShiftPgDn)
+ ) {
document.down(getHeight() - 1);
alignTopLine(true);
- } else if (keypress.equals(kbHome)) {
+ } else if (keypress.equals(kbHome)
+ || keypress.equals(kbShiftHome)
+ ) {
if (document.home()) {
leftColumn = 0;
if (leftColumn < 0) {
}
setCursorX(0);
}
- } else if (keypress.equals(kbEnd)) {
+ } else if (keypress.equals(kbEnd)
+ || keypress.equals(kbShiftEnd)
+ ) {
if (document.end()) {
alignCursor();
}
- } else if (keypress.equals(kbCtrlHome)) {
+ } else if (keypress.equals(kbCtrlHome)
+ || keypress.equals(kbCtrlShiftHome)
+ ) {
document.setLineNumber(0);
document.home();
topLine = 0;
leftColumn = 0;
setCursorX(0);
setCursorY(0);
- } else if (keypress.equals(kbCtrlEnd)) {
+ } else if (keypress.equals(kbCtrlEnd)
+ || keypress.equals(kbCtrlShiftEnd)
+ ) {
document.setLineNumber(document.getLineCount() - 1);
document.end();
alignTopLine(false);
} else if (keypress.equals(kbIns)) {
- document.setOverwrite(!document.getOverwrite());
+ document.setOverwrite(!document.isOverwrite());
} else if (keypress.equals(kbDel)) {
- document.del();
- alignCursor();
+ if (inSelection) {
+ deleteSelection();
+ alignCursor();
+ } else {
+ saveUndo();
+ document.del();
+ alignCursor();
+ }
} else if (keypress.equals(kbBackspace)
|| keypress.equals(kbBackspaceDel)
) {
- document.backspace();
- alignTopLine(false);
- } else if (keypress.equals(kbTab)) {
- // TODO: tab character. For now just add spaces until we hit
- // modulo 8.
- for (int i = document.getCursor(); (i + 1) % 8 != 0; i++) {
- document.addChar(' ');
+ if (inSelection) {
+ deleteSelection();
+ alignTopLine(false);
+ } else {
+ saveUndo();
+ document.backspace();
+ alignTopLine(false);
}
+ } else if (keypress.equals(kbTab)) {
+ deleteSelection();
+ saveUndo();
+ document.tab();
+ alignCursor();
+ } else if (keypress.equals(kbShiftTab)) {
+ deleteSelection();
+ saveUndo();
+ document.backTab();
alignCursor();
} else if (keypress.equals(kbEnter)) {
+ deleteSelection();
+ saveUndo();
document.enter();
alignTopLine(true);
} else if (!keypress.getKey().isFnKey()
&& !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();
+ }
}
/**
}
}
+ /**
+ * 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.
*
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 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.
*
document.saveToFilename(filename);
}
+ /**
+ * Delete text within the selection bounds.
+ */
+ private void deleteSelection() {
+ if (!inSelection) {
+ return;
+ }
+
+ saveUndo();
+
+ inSelection = false;
+
+ int startCol = selectionColumn0;
+ int startRow = selectionLine0;
+ int endCol = selectionColumn1;
+ int endRow = selectionLine1;
+
+ /*
+ System.err.println("INITIAL: " + startRow + " " + startCol + " " +
+ endRow + " " + endCol + " " +
+ document.getLineNumber() + " " + document.getCursor());
+ */
+
+ if (((selectionColumn1 < selectionColumn0)
+ && (selectionLine1 == selectionLine0))
+ || (selectionLine1 < selectionLine0)
+ ) {
+ // The user selected from bottom-to-top and/or right-to-left.
+ // Reverse the coordinates for the inverted section.
+ startCol = selectionColumn1;
+ startRow = selectionLine1;
+ endCol = selectionColumn0;
+ endRow = selectionLine0;
+
+ if (endRow >= document.getLineCount()) {
+ // The selection started beyond EOF, trim it to EOF.
+ endRow = document.getLineCount() - 1;
+ endCol = document.getLine(endRow).getDisplayLength();
+ } else if (endRow == document.getLineCount() - 1) {
+ // The selection started beyond EOF, trim it to EOF.
+ if (endCol >= document.getLine(endRow).getDisplayLength()) {
+ endCol = document.getLine(endRow).getDisplayLength() - 1;
+ }
+ }
+ }
+ /*
+ System.err.println("FLIP: " + startRow + " " + startCol + " " +
+ endRow + " " + endCol + " " +
+ document.getLineNumber() + " " + document.getCursor());
+ System.err.println(" --END: " + endRow + " " + document.getLineCount() +
+ " " + document.getLine(endRow).getDisplayLength());
+ */
+
+ assert (endRow < document.getLineCount());
+ if (endCol >= document.getLine(endRow).getDisplayLength()) {
+ endCol = document.getLine(endRow).getDisplayLength() - 1;
+ }
+ if (endCol < 0) {
+ endCol = 0;
+ }
+ if (startCol >= document.getLine(startRow).getDisplayLength()) {
+ startCol = document.getLine(startRow).getDisplayLength() - 1;
+ }
+ if (startCol < 0) {
+ startCol = 0;
+ }
+
+ // Place the cursor on the selection end, and "press backspace" until
+ // the cursor matches the selection start.
+ /*
+ System.err.println("BEFORE: " + startRow + " " + startCol + " " +
+ endRow + " " + endCol + " " +
+ document.getLineNumber() + " " + document.getCursor());
+ */
+ document.setLineNumber(endRow);
+ document.setCursor(endCol + 1);
+ while (!((document.getLineNumber() == startRow)
+ && (document.getCursor() == startCol))
+ ) {
+ /*
+ System.err.println("DURING: " + startRow + " " + startCol + " " +
+ endRow + " " + endCol + " " +
+ document.getLineNumber() + " " + document.getCursor());
+ */
+
+ document.backspace();
+ }
+ alignTopLine(true);
+ }
+
+ /**
+ * Copy text within the selection bounds to clipboard.
+ */
+ private void copySelection() {
+ if (!inSelection) {
+ return;
+ }
+ getClipboard().copyText(getSelection());
+ }
+
+ /**
+ * Set the selection.
+ *
+ * @param startRow the starting row number. 0-based: row 0 is the first
+ * row.
+ * @param startColumn the starting column number. 0-based: column 0 is
+ * the first column.
+ * @param endRow the ending row number. 0-based: row 0 is the first row.
+ * @param endColumn the ending column number. 0-based: column 0 is the
+ * first column.
+ */
+ public void setSelection(final int startRow, final int startColumn,
+ final int endRow, final int endColumn) {
+
+ inSelection = true;
+ selectionLine0 = startRow;
+ selectionColumn0 = startColumn;
+ selectionLine1 = endRow;
+ selectionColumn1 = endColumn;
+ }
+
+ /**
+ * Copy text within the selection bounds to a string.
+ *
+ * @return the selection as a string, or null if there is no selection
+ */
+ public String getSelection() {
+ if (!inSelection) {
+ return null;
+ }
+
+ int startCol = selectionColumn0;
+ int startRow = selectionLine0;
+ int endCol = selectionColumn1;
+ int endRow = selectionLine1;
+
+ if (((selectionColumn1 < selectionColumn0)
+ && (selectionLine1 == selectionLine0))
+ || (selectionLine1 < selectionLine0)
+ ) {
+ // The user selected from bottom-to-top and/or right-to-left.
+ // Reverse the coordinates for the inverted section.
+ startCol = selectionColumn1;
+ startRow = selectionLine1;
+ endCol = selectionColumn0;
+ endRow = selectionLine0;
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ if (endRow > startRow) {
+ // First line
+ String line = document.getLine(startRow).getRawString();
+ int x = 0;
+ for (int i = 0; i < line.length(); ) {
+ int ch = line.codePointAt(i);
+
+ if (x >= startCol) {
+ sb.append(Character.toChars(ch));
+ }
+ x += StringUtils.width(ch);
+ i += Character.charCount(ch);
+ }
+ sb.append("\n");
+
+ // Middle lines
+ for (int y = startRow + 1; y < endRow; y++) {
+ sb.append(document.getLine(y).getRawString());
+ sb.append("\n");
+ }
+
+ // Final line
+ line = document.getLine(endRow).getRawString();
+ x = 0;
+ for (int i = 0; i < line.length(); ) {
+ int ch = line.codePointAt(i);
+
+ if (x > endCol) {
+ break;
+ }
+
+ sb.append(Character.toChars(ch));
+ x += StringUtils.width(ch);
+ i += Character.charCount(ch);
+ }
+ } else {
+ assert (startRow == endRow);
+
+ // Only one line
+ String line = document.getLine(startRow).getRawString();
+ int x = 0;
+ for (int i = 0; i < line.length(); ) {
+ int ch = line.codePointAt(i);
+
+ if ((x >= startCol) && (x <= endCol)) {
+ sb.append(Character.toChars(ch));
+ }
+
+ x += StringUtils.width(ch);
+ i += Character.charCount(ch);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get the selection starting row number.
+ *
+ * @return the starting row number, or -1 if there is no selection.
+ * 0-based: row 0 is the first row.
+ */
+ public int getSelectionStartRow() {
+ if (!inSelection) {
+ return -1;
+ }
+
+ int startCol = selectionColumn0;
+ int startRow = selectionLine0;
+ int endCol = selectionColumn1;
+ int endRow = selectionLine1;
+
+ if (((selectionColumn1 < selectionColumn0)
+ && (selectionLine1 == selectionLine0))
+ || (selectionLine1 < selectionLine0)
+ ) {
+ // The user selected from bottom-to-top and/or right-to-left.
+ // Reverse the coordinates for the inverted section.
+ startCol = selectionColumn1;
+ startRow = selectionLine1;
+ endCol = selectionColumn0;
+ endRow = selectionLine0;
+ }
+ return startRow;
+ }
+
+ /**
+ * Get the selection starting column number.
+ *
+ * @return the starting column number, or -1 if there is no selection.
+ * 0-based: column 0 is the first column.
+ */
+ public int getSelectionStartColumn() {
+ if (!inSelection) {
+ return -1;
+ }
+
+ int startCol = selectionColumn0;
+ int startRow = selectionLine0;
+ int endCol = selectionColumn1;
+ int endRow = selectionLine1;
+
+ if (((selectionColumn1 < selectionColumn0)
+ && (selectionLine1 == selectionLine0))
+ || (selectionLine1 < selectionLine0)
+ ) {
+ // The user selected from bottom-to-top and/or right-to-left.
+ // Reverse the coordinates for the inverted section.
+ startCol = selectionColumn1;
+ startRow = selectionLine1;
+ endCol = selectionColumn0;
+ endRow = selectionLine0;
+ }
+ return startCol;
+ }
+
+ /**
+ * Get the selection ending row number.
+ *
+ * @return the ending row number, or -1 if there is no selection.
+ * 0-based: row 0 is the first row.
+ */
+ public int getSelectionEndRow() {
+ if (!inSelection) {
+ return -1;
+ }
+
+ int startCol = selectionColumn0;
+ int startRow = selectionLine0;
+ int endCol = selectionColumn1;
+ int endRow = selectionLine1;
+
+ if (((selectionColumn1 < selectionColumn0)
+ && (selectionLine1 == selectionLine0))
+ || (selectionLine1 < selectionLine0)
+ ) {
+ // The user selected from bottom-to-top and/or right-to-left.
+ // Reverse the coordinates for the inverted section.
+ startCol = selectionColumn1;
+ startRow = selectionLine1;
+ endCol = selectionColumn0;
+ endRow = selectionLine0;
+ }
+ return endRow;
+ }
+
+ /**
+ * Get the selection ending column number.
+ *
+ * @return the ending column number, or -1 if there is no selection.
+ * 0-based: column 0 is the first column.
+ */
+ public int getSelectionEndColumn() {
+ if (!inSelection) {
+ return -1;
+ }
+
+ int startCol = selectionColumn0;
+ int startRow = selectionLine0;
+ int endCol = selectionColumn1;
+ int endRow = selectionLine1;
+
+ if (((selectionColumn1 < selectionColumn0)
+ && (selectionLine1 == selectionLine0))
+ || (selectionLine1 < selectionLine0)
+ ) {
+ // The user selected from bottom-to-top and/or right-to-left.
+ // Reverse the coordinates for the inverted section.
+ startCol = selectionColumn1;
+ startRow = selectionLine1;
+ endCol = selectionColumn0;
+ endRow = selectionLine0;
+ }
+ return endCol;
+ }
+
+ /**
+ * Unset the selection.
+ */
+ public void unsetSelection() {
+ inSelection = false;
+ }
+
+ /**
+ * Replace whatever is being selected with new text. If not in
+ * selection, nothing is replaced.
+ *
+ * @param text the new replacement text
+ */
+ public void replaceSelection(final String text) {
+ if (!inSelection) {
+ return;
+ }
+
+ // Delete selected text, then paste text from clipboard.
+ deleteSelection();
+
+ for (int i = 0; i < text.length(); ) {
+ int ch = text.codePointAt(i);
+ switch (ch) {
+ case '\n':
+ onKeypress(new TKeypressEvent(kbEnter));
+ break;
+ case '\t':
+ onKeypress(new TKeypressEvent(kbTab));
+ break;
+ default:
+ if ((ch >= 0x20) && (ch != 0x7F)) {
+ onKeypress(new TKeypressEvent(false, 0, ch,
+ false, false, false));
+ }
+ break;
+ }
+ i += Character.charCount(ch);
+ }
+ }
+
+ /**
+ * Check if selection is available.
+ *
+ * @return true if a selection has been made
+ */
+ public boolean hasSelection() {
+ return inSelection;
+ }
+
+ /**
+ * Get the entire contents of the editor as one string.
+ *
+ * @return the editor contents
+ */
+ public String getText() {
+ return document.getText();
+ }
+
+ /**
+ * Set the entire contents of the editor from one string.
+ *
+ * @param text the new contents
+ */
+ public void setText(final String text) {
+ document = new Document(text, defaultColor);
+ unsetSelection();
+ topLine = 0;
+ leftColumn = 0;
+ }
+
+ // ------------------------------------------------------------------------
+ // EditMenuUser -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Check if the cut menu item should be enabled.
+ *
+ * @return true if the cut menu item should be enabled
+ */
+ public boolean isEditMenuCut() {
+ return true;
+ }
+
+ /**
+ * Check if the copy menu item should be enabled.
+ *
+ * @return true if the copy menu item should be enabled
+ */
+ public boolean isEditMenuCopy() {
+ return true;
+ }
+
+ /**
+ * Check if the paste menu item should be enabled.
+ *
+ * @return true if the paste menu item should be enabled
+ */
+ public boolean isEditMenuPaste() {
+ return true;
+ }
+
+ /**
+ * Check if the clear menu item should be enabled.
+ *
+ * @return true if the clear menu item should be enabled
+ */
+ public boolean isEditMenuClear() {
+ return true;
+ }
+
+ /**
+ * Save undo state.
+ */
+ private void saveUndo() {
+ SavedState state = new SavedState();
+ state.document = document.dup();
+ state.topLine = topLine;
+ state.leftColumn = leftColumn;
+ if (undoLevel > 0) {
+ while (undoList.size() > undoLevel) {
+ undoList.remove(0);
+ }
+ }
+ undoList.add(state);
+ undoListI = undoList.size() - 1;
+ }
+
+ /**
+ * Undo an edit.
+ */
+ public void undo() {
+ inSelection = false;
+ if ((undoListI >= 0) && (undoListI < undoList.size())) {
+ SavedState state = undoList.get(undoListI);
+ document = state.document.dup();
+ topLine = state.topLine;
+ leftColumn = state.leftColumn;
+ undoListI--;
+ setCursorY(document.getLineNumber() - topLine);
+ alignCursor();
+ }
+ }
+
+ /**
+ * Redo an edit.
+ */
+ public void redo() {
+ inSelection = false;
+ if ((undoListI >= 0) && (undoListI < undoList.size())) {
+ SavedState state = undoList.get(undoListI);
+ document = state.document.dup();
+ topLine = state.topLine;
+ leftColumn = state.leftColumn;
+ undoListI++;
+ setCursorY(document.getLineNumber() - topLine);
+ alignCursor();
+ }
+ }
+
+ /**
+ * Trim trailing whitespace from lines and trailing empty
+ * lines from the document.
+ */
+ public void cleanWhitespace() {
+ document.cleanWhitespace();
+ setCursorY(document.getLineNumber() - topLine);
+ alignCursor();
+ }
+
+ /**
+ * Set keyword highlighting.
+ *
+ * @param enabled if true, enable keyword highlighting
+ */
+ public void setHighlighting(final boolean enabled) {
+ document.setHighlighting(enabled);
+ }
+
}
import jexer.bits.GraphicsChars;
import jexer.event.TCommandEvent;
import jexer.event.TKeypressEvent;
+import jexer.event.TMenuEvent;
import jexer.event.TMouseEvent;
import jexer.event.TResizeEvent;
+import jexer.menu.TMenu;
import static jexer.TCommand.*;
import static jexer.TKeypress.*;
}
// ------------------------------------------------------------------------
- // TWindow ----------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
// ------------------------------------------------------------------------
/**
- * Draw the window.
+ * Called by application.switchWindow() when this window gets the
+ * focus, and also by application.addWindow().
*/
- @Override
- public void draw() {
- // Draw as normal.
- super.draw();
-
- // Add the row:col on the bottom row
- CellAttributes borderColor = getBorder();
- String location = String.format(" %d:%d ",
- editField.getEditingRowNumber(),
- editField.getEditingColumnNumber());
- int colon = location.indexOf(':');
- putStringXY(10 - colon, getHeight() - 1, location, borderColor);
+ public void onFocus() {
+ super.onFocus();
+ getApplication().enableMenuItem(TMenu.MID_UNDO);
+ getApplication().enableMenuItem(TMenu.MID_REDO);
+ }
- if (editField.isDirty()) {
- putCharXY(2, getHeight() - 1, GraphicsChars.OCTOSTAR, borderColor);
- }
+ /**
+ * Called by application.switchWindow() when another window gets the
+ * focus.
+ */
+ public void onUnfocus() {
+ super.onUnfocus();
+ getApplication().disableMenuItem(TMenu.MID_UNDO);
+ getApplication().disableMenuItem(TMenu.MID_REDO);
}
/**
super.onCommand(command);
}
+ /**
+ * Handle posted menu events.
+ *
+ * @param menu menu event
+ */
+ @Override
+ public void onMenu(final TMenuEvent menu) {
+ switch (menu.getId()) {
+ case TMenu.MID_UNDO:
+ editField.undo();
+ break;
+ case TMenu.MID_REDO:
+ editField.redo();
+ break;
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the window.
+ */
+ @Override
+ public void draw() {
+ // Draw as normal.
+ super.draw();
+
+ // Add the row:col on the bottom row
+ CellAttributes borderColor = getBorder();
+ String location = String.format(" %d:%d ",
+ editField.getEditingRowNumber(),
+ editField.getEditingColumnNumber());
+ int colon = location.indexOf(':');
+ putStringXY(10 - colon, getHeight() - 1, location, borderColor);
+
+ if (editField.isDirty()) {
+ putCharXY(2, getHeight() - 1, GraphicsChars.OCTOSTAR, borderColor);
+ }
+ }
+
/**
* Returns true if this window does not want the application-wide mouse
* cursor drawn over it.
final Throwable exception) {
super(application, i18n.getString("windowTitle"),
- 1, 1, 70, 20, CENTERED | MODAL);
+ 1, 1, 78, 22, CENTERED | MODAL);
this.exception = exception;
2, 6, "ttext", false);
ArrayList<String> stackTraceStrings = new ArrayList<String>();
+ stackTraceStrings.add(exception.getMessage());
StackTraceElement [] stack = exception.getStackTrace();
for (int i = 0; i < stack.length; i++) {
stackTraceStrings.add(stack[i].toString());
}
- stackTrace = addList(stackTraceStrings, 2, 7, getWidth() - 6, 8);
+ stackTrace = addList(stackTraceStrings, 2, 7, getWidth() - 6, 10);
// Buttons
- addButton(i18n.getString("saveButton"), 19, getHeight() - 4,
+ addButton(i18n.getString("saveButton"), 21, getHeight() - 4,
new TAction() {
public void DO() {
saveToFile();
});
TButton closeButton = addButton(i18n.getString("closeButton"),
- 35, getHeight() - 4,
+ 37, getHeight() - 4,
new TAction() {
public void DO() {
// Don't do anything, just close the window.
windowTitle=Java Exception Caught
statusBar=Exception
-captionLine1=An error has occurred. This may be due to a programming bug, but
-captionLine2=could also be a correctable or temporary issue. The stack trace
-captionLine3=is reported below. If you wish to submit a bug report, please
-captionLine4=use the Save button to create a more detailed error log.
+captionLine1=An error has occurred. This may be due to a programming bug, but could
+captionLine2=also be a correctable or temporary issue. The stack trace is reported
+captionLine3=below. If you wish to submit a bug report, please use the Save button
+captionLine4=to create a more detailed error log.
exceptionString={0}: {1}
import jexer.bits.CellAttributes;
import jexer.bits.GraphicsChars;
import jexer.bits.StringUtils;
+import jexer.event.TCommandEvent;
import jexer.event.TKeypressEvent;
import jexer.event.TMouseEvent;
+import static jexer.TCommand.*;
import static jexer.TKeypress.*;
/**
* TField implements an editable text field.
*/
-public class TField extends TWidget {
+public class TField extends TWidget implements EditMenuUser {
// ------------------------------------------------------------------------
// Variables --------------------------------------------------------------
if (keypress.equals(kbRight)) {
if (position < text.length()) {
+ int lastPosition = position;
screenPosition += StringUtils.width(text.codePointAt(position));
position += Character.charCount(text.codePointAt(position));
if (fixed == true) {
if (screenPosition == getWidth()) {
screenPosition--;
- position -= Character.charCount(text.codePointAt(position));
+ position -= Character.charCount(text.codePointAt(lastPosition));
}
} else {
while ((screenPosition - windowStart +
super.onKeypress(keypress);
}
+ /**
+ * Handle posted command events.
+ *
+ * @param command command event
+ */
+ @Override
+ public void onCommand(final TCommandEvent command) {
+ if (command.equals(cmCut)) {
+ // Copy text to clipboard, and then remove it.
+ getClipboard().copyText(text);
+ setText("");
+ return;
+ }
+
+ if (command.equals(cmCopy)) {
+ // Copy text to clipboard.
+ getClipboard().copyText(text);
+ return;
+ }
+
+ if (command.equals(cmPaste)) {
+ // Paste text from clipboard.
+ String newText = getClipboard().pasteText();
+ if (newText != null) {
+ setText(newText);
+ }
+ return;
+ }
+
+ if (command.equals(cmClear)) {
+ // Remove text.
+ setText("");
+ return;
+ }
+
+ }
+
// ------------------------------------------------------------------------
// TWidget ----------------------------------------------------------------
// ------------------------------------------------------------------------
assert (text != null);
this.text = text;
position = 0;
+ screenPosition = 0;
windowStart = 0;
+ if ((fixed == true) && (this.text.length() > getWidth())) {
+ this.text = this.text.substring(0, getWidth());
+ }
}
/**
updateAction = action;
}
+ // ------------------------------------------------------------------------
+ // EditMenuUser -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Check if the cut menu item should be enabled.
+ *
+ * @return true if the cut menu item should be enabled
+ */
+ public boolean isEditMenuCut() {
+ return true;
+ }
+
+ /**
+ * Check if the copy menu item should be enabled.
+ *
+ * @return true if the copy menu item should be enabled
+ */
+ public boolean isEditMenuCopy() {
+ return true;
+ }
+
+ /**
+ * Check if the paste menu item should be enabled.
+ *
+ * @return true if the paste menu item should be enabled
+ */
+ public boolean isEditMenuPaste() {
+ return true;
+ }
+
+ /**
+ * Check if the clear menu item should be enabled.
+ *
+ * @return true if the clear menu item should be enabled
+ */
+ public boolean isEditMenuClear() {
+ return true;
+ }
+
}
import java.awt.image.BufferedImage;
-import jexer.backend.ECMA48Terminal;
-import jexer.backend.MultiScreen;
-import jexer.backend.SwingTerminal;
import jexer.bits.Cell;
+import jexer.event.TCommandEvent;
import jexer.event.TKeypressEvent;
import jexer.event.TMouseEvent;
import jexer.event.TResizeEvent;
+import static jexer.TCommand.*;
import static jexer.TKeypress.*;
/**
* TImage renders a piece of a bitmap image on screen.
*/
-public class TImage extends TWidget {
+public class TImage extends TWidget implements EditMenuUser {
// ------------------------------------------------------------------------
// Constants --------------------------------------------------------------
resized = true;
}
+ /**
+ * Handle posted command events.
+ *
+ * @param command command event
+ */
+ @Override
+ public void onCommand(final TCommandEvent command) {
+ if (command.equals(cmCopy)) {
+ // Copy image to clipboard.
+ getClipboard().copyImage(image);
+ return;
+ }
+ }
+
// ------------------------------------------------------------------------
// TWidget ----------------------------------------------------------------
// ------------------------------------------------------------------------
}
Cell cell = new Cell();
- cell.setImage(image.getSubimage(x * textWidth,
- y * textHeight, width, height));
+ if ((width != textWidth) || (height != textHeight)) {
+ BufferedImage newImage;
+ newImage = new BufferedImage(textWidth, textHeight,
+ BufferedImage.TYPE_INT_ARGB);
+
+ java.awt.Graphics gr = newImage.getGraphics();
+ gr.drawImage(image.getSubimage(x * textWidth,
+ y * textHeight, width, height),
+ 0, 0, null, null);
+ gr.dispose();
+ cell.setImage(newImage);
+ } else {
+ cell.setImage(image.getSubimage(x * textWidth,
+ y * textHeight, width, height));
+ }
cells[x][y] = cell;
}
return newImage;
}
+ // ------------------------------------------------------------------------
+ // EditMenuUser -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Check if the cut menu item should be enabled.
+ *
+ * @return true if the cut menu item should be enabled
+ */
+ public boolean isEditMenuCut() {
+ return false;
+ }
+
+ /**
+ * Check if the copy menu item should be enabled.
+ *
+ * @return true if the copy menu item should be enabled
+ */
+ public boolean isEditMenuCopy() {
+ return true;
+ }
+
+ /**
+ * Check if the paste menu item should be enabled.
+ *
+ * @return true if the paste menu item should be enabled
+ */
+ public boolean isEditMenuPaste() {
+ return false;
+ }
+
+ /**
+ * Check if the clear menu item should be enabled.
+ *
+ * @return true if the clear menu item should be enabled
+ */
+ public boolean isEditMenuClear() {
+ return false;
+ }
+
}
public static final TKeypress kbAltShiftZ = new TKeypress(false,
0, 'Z', true, false, true);
+ public static final TKeypress kbAltShiftHome = new TKeypress(true,
+ TKeypress.HOME, ' ', true, false, true);
+ public static final TKeypress kbAltShiftEnd = new TKeypress(true,
+ TKeypress.END, ' ', true, false, true);
+ public static final TKeypress kbAltShiftPgUp = new TKeypress(true,
+ TKeypress.PGUP, ' ', true, false, true);
+ public static final TKeypress kbAltShiftPgDn = new TKeypress(true,
+ TKeypress.PGDN, ' ', true, false, true);
+ public static final TKeypress kbAltShiftUp = new TKeypress(true,
+ TKeypress.UP, ' ', true, false, true);
+ public static final TKeypress kbAltShiftDown = new TKeypress(true,
+ TKeypress.DOWN, ' ', true, false, true);
+ public static final TKeypress kbAltShiftLeft = new TKeypress(true,
+ TKeypress.LEFT, ' ', true, false, true);
+ public static final TKeypress kbAltShiftRight = new TKeypress(true,
+ TKeypress.RIGHT, ' ', true, false, true);
+
+ public static final TKeypress kbCtrlShiftHome = new TKeypress(true,
+ TKeypress.HOME, ' ', false, true, true);
+ public static final TKeypress kbCtrlShiftEnd = new TKeypress(true,
+ TKeypress.END, ' ', false, true, true);
+ public static final TKeypress kbCtrlShiftPgUp = new TKeypress(true,
+ TKeypress.PGUP, ' ', false, true, true);
+ public static final TKeypress kbCtrlShiftPgDn = new TKeypress(true,
+ TKeypress.PGDN, ' ', false, true, true);
+ public static final TKeypress kbCtrlShiftUp = new TKeypress(true,
+ TKeypress.UP, ' ', false, true, true);
+ public static final TKeypress kbCtrlShiftDown = new TKeypress(true,
+ TKeypress.DOWN, ' ', false, true, true);
+ public static final TKeypress kbCtrlShiftLeft = new TKeypress(true,
+ TKeypress.LEFT, ' ', false, true, true);
+ public static final TKeypress kbCtrlShiftRight = new TKeypress(true,
+ TKeypress.RIGHT, ' ', false, true, true);
+
+
/**
* Backspace as ^H.
*/
return "\u25C0\u2500\u2518";
}
+ // Special case: Space is "Space"
+ if (equals(kbSpace)) {
+ return "Space";
+ }
+
if (equals(kbShiftLeft)) {
return "Shift+\u2190";
}
@Override
public void setWidth(final int width) {
super.setWidth(width);
- hScroller.setWidth(getWidth() - 1);
- vScroller.setX(getWidth() - 1);
+ if (hScroller != null) {
+ hScroller.setWidth(getWidth() - 1);
+ }
+ if (vScroller != null) {
+ vScroller.setX(getWidth() - 1);
+ }
}
/**
* Override TWidget's height: we need to set child widget heights.
- * time.
*
* @param height new widget height
*/
@Override
public void setHeight(final int height) {
super.setHeight(height);
- hScroller.setY(getHeight() - 1);
- vScroller.setHeight(getHeight() - 1);
+ if (hScroller != null) {
+ hScroller.setY(getHeight() - 1);
+ }
+ if (vScroller != null) {
+ vScroller.setHeight(getHeight() - 1);
+ }
}
/**
int topY = 0;
for (int i = begin; i < strings.size(); i++) {
String line = strings.get(i);
+ if (line == null) {
+ line = "";
+ }
if (getHorizontalValue() < line.length()) {
line = line.substring(getHorizontalValue());
} else {
package jexer;
import jexer.bits.CellAttributes;
-import jexer.bits.GraphicsChars;
import jexer.bits.StringUtils;
/**
// ------------------------------------------------------------------------
/**
- * RadioButton state, true means selected.
+ * RadioButton state, true means selected. Note package private access.
*/
- private boolean selected = false;
+ boolean selected = false;
/**
* The shortcut and radio button label.
/**
* ID for this radio button. Buttons start counting at 1 in the
- * RadioGroup.
+ * RadioGroup. Note package private access.
*/
- private int id;
+ int id;
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
/**
- * Public constructor.
+ * Package private constructor.
*
* @param parent parent widget
* @param x column relative to parent
* @param label label to display next to (right of) the radiobutton
* @param id ID for this radio button
*/
- public TRadioButton(final TRadioGroup parent, final int x, final int y,
+ TRadioButton(final TRadioGroup parent, final int x, final int y,
final String label, final int id) {
// Set parent and window
setCursorVisible(true);
setCursorX(1);
+
+ parent.addRadioButton(this);
}
// ------------------------------------------------------------------------
public void onMouseDown(final TMouseEvent mouse) {
if ((mouseOnRadioButton(mouse)) && (mouse.isMouse1())) {
// Switch state
- selected = true;
- ((TRadioGroup) getParent()).setSelected(this);
+ ((TRadioGroup) getParent()).setSelected(id);
}
}
public void onKeypress(final TKeypressEvent keypress) {
if (keypress.equals(kbSpace)) {
- selected = true;
- ((TRadioGroup) getParent()).setSelected(this);
+ ((TRadioGroup) getParent()).setSelected(id);
return;
}
}
/**
- * Set RadioButton state, true means selected. Note package private
- * access.
+ * Set RadioButton state, true means selected.
*
* @param selected if true then this is the one button in the group that
* is selected
*/
- void setSelected(final boolean selected) {
- this.selected = selected;
+ public void setSelected(final boolean selected) {
+ if (selected == true) {
+ ((TRadioGroup) getParent()).setSelected(id);
+ } else {
+ ((TRadioGroup) getParent()).setSelected(0);
+ }
}
/**
* If true, one of the children MUST be selected. Note package private
* access.
*/
- boolean requiresSelection = true;
+ boolean requiresSelection = false;
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of group
+ * @param label label to display on the group box
+ */
+ public TRadioGroup(final TWidget parent, final int x, final int y,
+ final int width, final String label) {
+
+ // Set parent and window
+ super(parent, x, y, width, 2);
+
+ this.label = label;
+ }
+
/**
* Public constructor.
*
return selectedButton.getId();
}
- /**
- * Set the new selected radio button. Note package private access.
- *
- * @param button new button that became selected
- */
- void setSelected(final TRadioButton button) {
- assert (button.isSelected());
- if ((selectedButton != null) && (selectedButton != button)) {
- selectedButton.setSelected(false);
- }
- selectedButton = button;
- }
-
/**
* Set the new selected radio button. 1-based.
*
return;
}
+ for (TWidget widget: getChildren()) {
+ ((TRadioButton) widget).selected = false;
+ }
if (id == 0) {
- for (TWidget widget: getChildren()) {
- ((TRadioButton) widget).setSelected(false);
- }
selectedButton = null;
return;
}
assert ((id > 0) && (id <= getChildren().size()));
TRadioButton button = (TRadioButton) (getChildren().get(id - 1));
- button.setSelected(true);
+ button.selected = true;
selectedButton = button;
}
+ /**
+ * Get the radio button that was selected.
+ *
+ * @return the selected button, or null if no button is selected
+ */
+ public TRadioButton getSelectedButton() {
+ return selectedButton;
+ }
+
+ /**
+ * Convenience function to add a radio button to this group.
+ *
+ * @param label label to display next to (right of) the radiobutton
+ * @param selected if true, this will be the selected radiobutton
+ * @return the new radio button
+ */
+ public TRadioButton addRadioButton(final String label,
+ final boolean selected) {
+
+ TRadioButton button = addRadioButton(label);
+ setSelected(button.id);
+ return button;
+ }
+
/**
* Convenience function to add a radio button to this group.
*
* @return the new radio button
*/
public TRadioButton addRadioButton(final String label) {
- int buttonX = 1;
- int buttonY = getChildren().size() + 1;
+ return new TRadioButton(this, 0, 0, label, 0);
+ }
+
+ /**
+ * Package private method for RadioButton to add itself to a RadioGroup
+ * container.
+ *
+ * @param button the button to add
+ */
+ void addRadioButton(final TRadioButton button) {
+ super.setHeight(getChildren().size() + 2);
+ button.setX(1);
+ button.setY(getChildren().size());
+ button.id = getChildren().size();
+ String label = button.getMnemonic().getRawLabel();
+
if (StringUtils.width(label) + 4 > getWidth()) {
super.setWidth(StringUtils.width(label) + 7);
}
- super.setHeight(getChildren().size() + 3);
- TRadioButton button = new TRadioButton(this, buttonX, buttonY, label,
- getChildren().size() + 1);
if (getParent().getLayoutManager() != null) {
getParent().getLayoutManager().resetSize(this);
// Default to the first item on the list.
activate(getChildren().get(0));
+ }
- return button;
+ /**
+ * Get the requires selection flag.
+ *
+ * @return true if this radiogroup requires that one of the buttons be
+ * selected
+ */
+ public boolean getRequiresSelection() {
+ return requiresSelection;
+ }
+
+ /**
+ * Set the requires selection flag.
+ *
+ * @param requiresSelection if true, then this radiogroup requires that
+ * one of the buttons be selected
+ */
+ public void setRequiresSelection(final boolean requiresSelection) {
+ this.requiresSelection = requiresSelection;
+ if (requiresSelection) {
+ if ((getChildren().size() > 0) && (selectedButton == null)) {
+ setSelected(1);
+ }
+ }
}
}
import jexer.bits.CellAttributes;
import jexer.bits.GraphicsChars;
-import jexer.event.TMenuEvent;
import jexer.event.TMouseEvent;
import jexer.event.TResizeEvent;
-import jexer.menu.TMenu;
/**
* TSplitPane contains two widgets with a draggable horizontal or vertical
CellAttributes attr = getTheme().getColor("tsplitpane");
if (vertical) {
vLineXY(split, 0, getHeight(), GraphicsChars.WINDOW_SIDE, attr);
- // TODO: draw intersections of children
+
+ // Draw intersections of children
+ if ((left instanceof TSplitPane)
+ && (((TSplitPane) left).vertical == false)
+ && (right instanceof TSplitPane)
+ && (((TSplitPane) right).vertical == false)
+ && (((TSplitPane) left).split == ((TSplitPane) right).split)
+ ) {
+ putCharXY(split, ((TSplitPane) left).split, '\u253C', attr);
+ } else {
+ if ((left instanceof TSplitPane)
+ && (((TSplitPane) left).vertical == false)
+ ) {
+ putCharXY(split, ((TSplitPane) left).split, '\u2524', attr);
+ }
+ if ((right instanceof TSplitPane)
+ && (((TSplitPane) right).vertical == false)
+ ) {
+ putCharXY(split, ((TSplitPane) right).split, '\u251C',
+ attr);
+ }
+ }
if ((mouse != null)
&& (mouse.getAbsoluteX() == getAbsoluteX() + split)
}
} else {
hLineXY(0, split, getWidth(), GraphicsChars.SINGLE_BAR, attr);
- // TODO: draw intersections of children
+
+ // Draw intersections of children
+ if ((top instanceof TSplitPane)
+ && (((TSplitPane) top).vertical == true)
+ && (bottom instanceof TSplitPane)
+ && (((TSplitPane) bottom).vertical == true)
+ && (((TSplitPane) top).split == ((TSplitPane) bottom).split)
+ ) {
+ putCharXY(((TSplitPane) top).split, split, '\u253C', attr);
+ } else {
+ if ((top instanceof TSplitPane)
+ && (((TSplitPane) top).vertical == true)
+ ) {
+ putCharXY(((TSplitPane) top).split, split, '\u2534', attr);
+ }
+ if ((bottom instanceof TSplitPane)
+ && (((TSplitPane) bottom).vertical == true)
+ ) {
+ putCharXY(((TSplitPane) bottom).split, split, '\u252C',
+ attr);
+ }
+ }
if ((mouse != null)
&& (mouse.getAbsoluteY() == getAbsoluteY() + split)
keep.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
getHeight()));
}
-
+
return keep;
}
for (int i = 0; i < list.size(); i++) {
rows.get(selectedRow).get(i).setText(list.get(i));
}
-
- // TODO: detect header line
}
} finally {
if (reader != null) {
*/
public void onFocus() {
// Enable the table menu items.
- getApplication().enableMenuItem(TMenu.MID_CUT);
getApplication().enableMenuItem(TMenu.MID_TABLE_RENAME_COLUMN);
getApplication().enableMenuItem(TMenu.MID_TABLE_RENAME_ROW);
getApplication().enableMenuItem(TMenu.MID_TABLE_VIEW_ROW_LABELS);
*/
public void onUnfocus() {
// Disable the table menu items.
- getApplication().disableMenuItem(TMenu.MID_CUT);
getApplication().disableMenuItem(TMenu.MID_TABLE_RENAME_COLUMN);
getApplication().disableMenuItem(TMenu.MID_TABLE_RENAME_ROW);
getApplication().disableMenuItem(TMenu.MID_TABLE_VIEW_ROW_LABELS);
*/
package jexer;
-import java.awt.Font;
-import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
-
-import java.io.InputStream;
+import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import jexer.backend.ECMA48Terminal;
import jexer.backend.GlyphMaker;
-import jexer.backend.MultiScreen;
import jexer.backend.SwingTerminal;
import jexer.bits.Cell;
-import jexer.bits.CellAttributes;
+import jexer.event.TCommandEvent;
import jexer.event.TKeypressEvent;
import jexer.event.TMenuEvent;
import jexer.event.TMouseEvent;
import jexer.tterminal.DisplayLine;
import jexer.tterminal.DisplayListener;
import jexer.tterminal.ECMA48;
+import static jexer.TCommand.*;
import static jexer.TKeypress.*;
/**
* TTerminalWidget exposes a ECMA-48 / ANSI X3.64 style terminal in a widget.
*/
public class TTerminalWidget extends TScrollableWidget
- implements DisplayListener {
+ implements DisplayListener, EditMenuUser {
/**
* Translated strings.
*/
private Process shell;
+ /**
+ * If true, something called 'ptypipe' is on the PATH and executable.
+ */
+ private static boolean ptypipeOnPath = false;
+
/**
* If true, we are using the ptypipe utility to support dynamic window
* resizing. ptypipe is available at
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
+ /**
+ * Static constructor.
+ */
+ static {
+ checkForPtypipe();
+ }
+
/**
* Public constructor spawns a custom command line.
*
* @param x column relative to parent
* @param y row relative to parent
* @param command the command line to execute
- * @param closeAction action to perform when the shell sxits
+ * @param closeAction action to perform when the shell exits
*/
public TTerminalWidget(final TWidget parent, final int x, final int y,
final String [] command, final TAction closeAction) {
* @param width width of widget
* @param height height of widget
* @param command the command line to execute
- * @param closeAction action to perform when the shell sxits
+ * @param closeAction action to perform when the shell exits
*/
public TTerminalWidget(final TWidget parent, final int x, final int y,
final int width, final int height, final String [] command,
fullCommand = new String[command.length + 1];
fullCommand[0] = "ptypipe";
System.arraycopy(command, 0, fullCommand, 1, command.length);
+ } else if (System.getProperty("jexer.TTerminal.ptypipe",
+ "auto").equals("auto")
+ && (ptypipeOnPath == true)
+ ) {
+ ptypipe = true;
+ fullCommand = new String[command.length + 1];
+ fullCommand[0] = "ptypipe";
+ System.arraycopy(command, 0, fullCommand, 1, command.length);
} else if (System.getProperty("os.name").startsWith("Windows")) {
fullCommand = new String[3];
fullCommand[0] = "cmd";
fullCommand[5] = stringArrayToString(command);
} else {
// Default: behave like Linux
- fullCommand = new String[5];
- fullCommand[0] = "script";
- fullCommand[1] = "-fqe";
- fullCommand[2] = "/dev/null";
- fullCommand[3] = "-c";
- fullCommand[4] = stringArrayToString(command);
+ if (System.getProperty("jexer.TTerminal.setsid",
+ "true").equals("false")
+ ) {
+ fullCommand = new String[5];
+ fullCommand[0] = "script";
+ fullCommand[1] = "-fqe";
+ fullCommand[2] = "/dev/null";
+ fullCommand[3] = "-c";
+ fullCommand[4] = stringArrayToString(command);
+ } else {
+ fullCommand = new String[6];
+ fullCommand[0] = "setsid";
+ fullCommand[1] = "script";
+ fullCommand[2] = "-fqe";
+ fullCommand[3] = "/dev/null";
+ fullCommand[4] = "-c";
+ fullCommand[5] = stringArrayToString(command);
+ }
}
spawnShell(fullCommand);
}
* @param parent parent widget
* @param x column relative to parent
* @param y row relative to parent
- * @param closeAction action to perform when the shell sxits
+ * @param closeAction action to perform when the shell exits
*/
public TTerminalWidget(final TWidget parent, final int x, final int y,
final TAction closeAction) {
* @param y row relative to parent
* @param width width of widget
* @param height height of widget
- * @param closeAction action to perform when the shell sxits
+ * @param closeAction action to perform when the shell exits
*/
public TTerminalWidget(final TWidget parent, final int x, final int y,
final int width, final int height, final TAction closeAction) {
// GNU differ on the '-f' vs '-F' flags, we need two different
// commands. Lovely.
String cmdShellGNU = "script -fqe /dev/null";
+ String cmdShellGNUSetsid = "setsid script -fqe /dev/null";
String cmdShellBSD = "script -q -F /dev/null";
// ptypipe is another solution that permits dynamic window resizing.
) {
ptypipe = true;
spawnShell(cmdShellPtypipe.split("\\s+"));
+ } else if (System.getProperty("jexer.TTerminal.ptypipe",
+ "auto").equals("auto")
+ && (ptypipeOnPath == true)
+ ) {
+ ptypipe = true;
+ spawnShell(cmdShellPtypipe.split("\\s+"));
} else if (System.getProperty("os.name").startsWith("Windows")) {
spawnShell(cmdShellWindows.split("\\s+"));
} else if (System.getProperty("os.name").startsWith("Mac")) {
spawnShell(cmdShellBSD.split("\\s+"));
} else if (System.getProperty("os.name").startsWith("Linux")) {
- spawnShell(cmdShellGNU.split("\\s+"));
+ if (System.getProperty("jexer.TTerminal.setsid",
+ "true").equals("false")
+ ) {
+ spawnShell(cmdShellGNU.split("\\s+"));
+ } else {
+ spawnShell(cmdShellGNUSetsid.split("\\s+"));
+ }
} else {
// When all else fails, assume GNU.
spawnShell(cmdShellGNU.split("\\s+"));
super.onMouseMotion(mouse);
}
+ /**
+ * Handle posted command events.
+ *
+ * @param command command event
+ */
+ @Override
+ public void onCommand(final TCommandEvent command) {
+ if (emulator == null) {
+ return;
+ }
+
+ if (command.equals(cmPaste)) {
+ // Paste text from clipboard.
+ String text = getClipboard().pasteText();
+ if (text != null) {
+ for (int i = 0; i < text.length(); ) {
+ int ch = text.codePointAt(i);
+ emulator.addUserEvent(new TKeypressEvent(false, 0, ch,
+ false, false, false));
+ i += Character.charCount(ch);
+ }
+ }
+ return;
+ }
+ }
+
// ------------------------------------------------------------------------
// TScrollableWidget ------------------------------------------------------
// ------------------------------------------------------------------------
int width = getDisplayWidth();
boolean syncEmulator = false;
- if ((System.currentTimeMillis() - lastUpdateTime >= 20)
- && (dirty == true)
- ) {
+ if (System.currentTimeMillis() - lastUpdateTime >= 50) {
// Too much time has passed, draw it all.
syncEmulator = true;
} else if (emulator.isReading() && (dirty == false)) {
// TTerminalWidget --------------------------------------------------------
// ------------------------------------------------------------------------
+ /**
+ * Check for 'ptypipe' on the path. If available, set ptypipeOnPath.
+ */
+ private static void checkForPtypipe() {
+ String systemPath = System.getenv("PATH");
+ if (systemPath == null) {
+ return;
+ }
+
+ String [] paths = systemPath.split(File.pathSeparator);
+ if (paths == null) {
+ return;
+ }
+ if (paths.length == 0) {
+ return;
+ }
+ for (int i = 0; i < paths.length; i++) {
+ File path = new File(paths[i]);
+ if (path.exists() && path.isDirectory()) {
+ File [] files = path.listFiles();
+ if (files == null) {
+ continue;
+ }
+ if (files.length == 0) {
+ continue;
+ }
+ for (int j = 0; j < files.length; j++) {
+ File file = files[j];
+ if (file.canExecute() && file.getName().equals("ptypipe")) {
+ ptypipeOnPath = true;
+ return;
+ }
+ }
+ }
+ }
+ }
+
/**
* Get the desired window title.
*
}
});
}
- if (getApplication() != null) {
- getApplication().postEvent(new TMenuEvent(
- TMenu.MID_REPAINT));
- }
+ app.doRepaint();
}
}
} // synchronized (emulator)
}
+ /**
+ * Wait for a period of time to get output from the launched process.
+ *
+ * @param millis millis to wait for, or 0 to wait forever
+ * @return true if the launched process has emitted something
+ */
+ public boolean waitForOutput(final int millis) {
+ if (emulator == null) {
+ return false;
+ }
+ return emulator.waitForOutput(millis);
+ }
+
/**
* Check if a mouse press/release/motion event coordinate is over the
* emulator.
* Called by emulator when fresh data has come in.
*/
public void displayChanged() {
- dirty = true;
+ if (emulator != null) {
+ // Force sync here: EMCA48.run() thread might be setting
+ // dirty=true while TTerminalWdiget.draw() is setting
+ // dirty=false. If these writes start interleaving, the display
+ // stops getting updated.
+ synchronized (emulator) {
+ dirty = true;
+ }
+ } else {
+ dirty = true;
+ }
getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
}
return 24;
}
+ /**
+ * Get the exit value for the emulator.
+ *
+ * @return exit value
+ */
+ public int getExitValue() {
+ return exitValue;
+ }
+
+ // ------------------------------------------------------------------------
+ // EditMenuUser -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Check if the cut menu item should be enabled.
+ *
+ * @return true if the cut menu item should be enabled
+ */
+ public boolean isEditMenuCut() {
+ return false;
+ }
+
+ /**
+ * Check if the copy menu item should be enabled.
+ *
+ * @return true if the copy menu item should be enabled
+ */
+ public boolean isEditMenuCopy() {
+ return false;
+ }
+
+ /**
+ * Check if the paste menu item should be enabled.
+ *
+ * @return true if the paste menu item should be enabled
+ */
+ public boolean isEditMenuPaste() {
+ return true;
+ }
+
+ /**
+ * Check if the clear menu item should be enabled.
+ *
+ * @return true if the clear menu item should be enabled
+ */
+ public boolean isEditMenuClear() {
+ return false;
+ }
+
}
*/
package jexer;
-import java.awt.Font;
-import java.awt.FontMetrics;
-import java.awt.Graphics2D;
-import java.awt.image.BufferedImage;
-
-import java.io.InputStream;
-import java.io.IOException;
-import java.lang.reflect.Field;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
import java.util.ResourceBundle;
-import jexer.backend.ECMA48Terminal;
-import jexer.backend.GlyphMaker;
-import jexer.backend.MultiScreen;
-import jexer.backend.SwingTerminal;
-import jexer.bits.Cell;
-import jexer.bits.CellAttributes;
+import jexer.menu.TMenu;
import jexer.event.TKeypressEvent;
import jexer.event.TMenuEvent;
import jexer.event.TMouseEvent;
import jexer.event.TResizeEvent;
-import jexer.menu.TMenu;
-import jexer.tterminal.DisplayLine;
-import jexer.tterminal.DisplayListener;
-import jexer.tterminal.ECMA48;
+import static jexer.TCommand.*;
import static jexer.TKeypress.*;
/**
addShortcutKeys();
// Add shortcut text
- newStatusBar(i18n.getString("statusBarRunning"));
+ TStatusBar statusBar = newStatusBar(i18n.getString("statusBarRunning"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp,
+ i18n.getString("statusBarHelp"));
+ statusBar.addShortcutKeypress(kbF10, cmMenu,
+ i18n.getString("statusBarMenu"));
// Spin it up
- terminal = new TTerminalWidget(this, 0, 0, new TAction() {
+ terminal = new TTerminalWidget(this, 0, 0, command, new TAction() {
public void DO() {
onShellExit();
}
addShortcutKeys();
// Add shortcut text
- newStatusBar(i18n.getString("statusBarRunning"));
+ TStatusBar statusBar = newStatusBar(i18n.getString("statusBarRunning"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp,
+ i18n.getString("statusBarHelp"));
+ statusBar.addShortcutKeypress(kbF10, cmMenu,
+ i18n.getString("statusBarMenu"));
// Spin it up
terminal = new TTerminalWidget(this, 0, 0, new TAction() {
*/
@Override
public void onKeypress(final TKeypressEvent keypress) {
- if ((terminal != null) && (terminal.isReading())) {
+ if ((terminal != null)
+ && (terminal.isReading())
+ && (!inKeyboardResize)
+ ) {
terminal.onKeypress(keypress);
} else {
super.onKeypress(keypress);
}
}
+ /**
+ * Get this window's help topic to load.
+ *
+ * @return the topic name
+ */
+ @Override
+ public String getHelpTopic() {
+ return "Terminal Window";
+ }
+
// ------------------------------------------------------------------------
// TTerminalWindow --------------------------------------------------------
// ------------------------------------------------------------------------
getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
}
+ /**
+ * Wait for a period of time to get output from the launched process.
+ *
+ * @param millis millis to wait for, or 0 to wait forever
+ * @return true if the launched process has emitted something
+ */
+ public boolean waitForOutput(final int millis) {
+ if (terminal == null) {
+ return false;
+ }
+ return terminal.waitForOutput(millis);
+ }
+
+ /**
+ * Get the exit value for the emulator.
+ *
+ * @return exit value
+ */
+ public int getExitValue() {
+ if (terminal == null) {
+ return -1;
+ }
+ return terminal.getExitValue();
+ }
+
}
windowTitle=Terminal
statusBarRunning=Terminal session executing...
+statusBarHelp=Help
+statusBarMenu=Menu
package jexer;
import java.util.Arrays;
-import java.util.LinkedList;
+import java.util.ArrayList;
import java.util.List;
import jexer.bits.CellAttributes;
this.text = text;
this.colorKey = colorKey;
- lines = new LinkedList<String>();
+ lines = new ArrayList<String>();
vScroller = new TVScroller(this, getWidth() - 1, 0,
Math.max(1, getHeight() - 1));
/**
* Set justification.
*
- * @param justification LEFT, CENTER, RIGHT, or FULL
+ * @param justification NONE, LEFT, CENTER, RIGHT, or FULL
*/
public void setJustification(final Justification justification) {
this.justification = justification;
reflowData();
}
+ /**
+ * Un-justify the text.
+ */
+ public void unJustify() {
+ justification = Justification.NONE;
+ reflowData();
+ }
+
}
import jexer.backend.Screen;
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
import jexer.bits.ColorTheme;
import jexer.event.TCommandEvent;
import jexer.event.TInputEvent;
* @param command command event
*/
public void onCommand(final TCommandEvent command) {
- // Default: do nothing, pass to children instead
- for (TWidget widget: children) {
- widget.onCommand(command);
+ if (activeChild != null) {
+ activeChild.onCommand(command);
}
}
return null;
}
+ /**
+ * Get the Clipboard.
+ *
+ * @return the Clipboard, or null if not assigned
+ */
+ public Clipboard getClipboard() {
+ if (window != null) {
+ return window.getApplication().getClipboard();
+ }
+ return null;
+ }
+
/**
* Comparison operator. For various subclasses it sorts on:
* <ul>
* difference between this.z and that.z, or String.compareTo(text)
*/
@Override
- public final int compareTo(final TWidget that) {
+ public int compareTo(final TWidget that) {
if ((this instanceof TWindow)
&& (that instanceof TWindow)
) {
if (activeChild != null) {
activeChild.active = false;
}
- child.active = true;
- activeChild = child;
}
+ child.active = true;
+ activeChild = child;
}
}
return new TRadioGroup(this, x, y, label);
}
+ /**
+ * Convenience function to add a radio button group to this
+ * container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of group
+ * @param label label to display on the group box
+ */
+ public final TRadioGroup addRadioGroup(final int x, final int y,
+ final int width, final String label) {
+
+ return new TRadioGroup(this, x, y, width, label);
+ }
+
/**
* Convenience function to add a text field to this container/window.
*
*/
private boolean hideMouse = false;
+ /**
+ * The help topic for this window.
+ */
+ protected String helpTopic = "Help";
+
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
}
if (inWindowResize) {
- // Do not permit resizing below the status line
- if (mouse.getAbsoluteY() == application.getDesktopBottom()) {
- inWindowResize = false;
- return;
- }
-
// Move window over
setWidth(resizeWindowWidth + (mouse.getAbsoluteX()
- moveWindowMouseX));
// Keep within min/max bounds
if (getWidth() < minimumWindowWidth) {
setWidth(minimumWindowWidth);
- inWindowResize = false;
}
if (getHeight() < minimumWindowHeight) {
setHeight(minimumWindowHeight);
- inWindowResize = false;
}
if ((maximumWindowWidth > 0)
&& (getWidth() > maximumWindowWidth)
) {
setWidth(maximumWindowWidth);
- inWindowResize = false;
}
if ((maximumWindowHeight > 0)
&& (getHeight() > maximumWindowHeight)
) {
setHeight(maximumWindowHeight);
- inWindowResize = false;
+ }
+ if (getHeight() + getY() >= getApplication().getDesktopBottom()) {
+ setHeight(getApplication().getDesktopBottom() - getY());
}
// Pass a resize event to my children
this.hideMouse = hideMouse;
}
+ /**
+ * Get this window's help topic to load.
+ *
+ * @return the topic name
+ */
+ public String getHelpTopic() {
+ return helpTopic;
+ }
+
/**
* Generate a human-readable string for this window.
*
*/
@Override
public String toString() {
- return String.format("%s(%8x) \'%s\' position (%d, %d) geometry %dx%d" +
- " hidden %s modal %s", getClass().getName(), hashCode(), title,
+ return String.format("%s(%8x) \'%s\' Z %d position (%d, %d) " +
+ "geometry %dx%d hidden %s modal %s",
+ getClass().getName(), hashCode(), title, getZ(),
getX(), getY(), getWidth(), getHeight(), hidden, isModal());
}
*/
package jexer.backend;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.util.List;
import javax.imageio.ImageIO;
-import jexer.TImage;
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
import jexer.bits.Color;
+import jexer.bits.StringUtils;
import jexer.event.TCommandEvent;
import jexer.event.TInputEvent;
import jexer.event.TKeypressEvent;
MOUSE_SGR,
}
+ /**
+ * Available Jexer images support.
+ */
+ private enum JexerImageOption {
+ DISABLED,
+ JPG,
+ PNG,
+ RGB,
+ }
+
// ------------------------------------------------------------------------
// Variables --------------------------------------------------------------
// ------------------------------------------------------------------------
*/
private boolean sixel = true;
+ /**
+ * If true, use a single shared palette for sixel.
+ */
+ private boolean sixelSharedPalette = true;
+
/**
* The sixel palette handler.
*/
private ImageCache iterm2Cache = null;
/**
- * If true, emit image data via Jexer image protocol.
+ * If not DISABLED, emit image data via Jexer image protocol if the
+ * terminal supports it.
*/
- private boolean jexerImages = false;
+ private JexerImageOption jexerImageOption = JexerImageOption.JPG;
/**
* The Jexer post-rendered string cache.
*/
private ImageCache jexerCache = null;
- /**
- * Base64 encoder used by iTerm2 and Jexer images.
- */
- private java.util.Base64.Encoder base64 = null;
-
/**
* If true, then we changed System.in and need to change it back.
*/
// Enable mouse reporting and metaSendsEscape
this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
- this.output.flush();
// Request xterm use the sixel settings we want
this.output.printf("%s", xtermSetSixelSettings());
+ this.output.flush();
+
// Query the screen size
sessionInfo.queryWindowSize();
setDimensions(sessionInfo.getWindowWidth(),
// Enable mouse reporting and metaSendsEscape
this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
- this.output.flush();
// Request xterm use the sixel settings we want
this.output.printf("%s", xtermSetSixelSettings());
+ this.output.flush();
+
// Query the screen size
sessionInfo.queryWindowSize();
setDimensions(sessionInfo.getWindowWidth(),
// SQUASH
}
- // Default to using images for full-width characters.
+ // Shared palette
+ if (System.getProperty("jexer.ECMA48.sixelSharedPalette",
+ "true").equals("false")) {
+ sixelSharedPalette = false;
+ } else {
+ sixelSharedPalette = true;
+ }
+
+ // Default to not supporting iTerm2 images.
if (System.getProperty("jexer.ECMA48.iTerm2Images",
"false").equals("true")) {
iterm2Images = true;
iterm2Images = false;
}
+ // Default to using JPG Jexer images if terminal supports it.
+ String jexerImageStr = System.getProperty("jexer.ECMA48.jexerImages",
+ "jpg").toLowerCase();
+ if (jexerImageStr.equals("false")) {
+ jexerImageOption = JexerImageOption.DISABLED;
+ } else if (jexerImageStr.equals("jpg")) {
+ jexerImageOption = JexerImageOption.JPG;
+ } else if (jexerImageStr.equals("png")) {
+ jexerImageOption = JexerImageOption.PNG;
+ } else if (jexerImageStr.equals("rgb")) {
+ jexerImageOption = JexerImageOption.RGB;
+ }
+
// Set custom colors
setCustomSystemColors();
}
* @return the width in pixels of a character cell
*/
public int getTextWidth() {
- return (widthPixels / sessionInfo.getWindowWidth());
+ if (sessionInfo.getWindowWidth() > 0) {
+ return (widthPixels / sessionInfo.getWindowWidth());
+ }
+ return 16;
}
/**
* @return the height in pixels of a character cell
*/
public int getTextHeight() {
- return (heightPixels / sessionInfo.getWindowHeight());
+ if (sessionInfo.getWindowHeight() > 0) {
+ return (heightPixels / sessionInfo.getWindowHeight());
+ }
+ return 20;
}
/**
if (cellsToDraw.size() > 0) {
if (iterm2Images) {
sb.append(toIterm2Image(x, y, cellsToDraw));
- } else if (jexerImages) {
+ } else if (jexerImageOption != JexerImageOption.DISABLED) {
sb.append(toJexerImage(x, y, cellsToDraw));
} else {
sb.append(toSixel(x, y, cellsToDraw));
boolean eventMouse3 = false;
boolean eventMouseWheelUp = false;
boolean eventMouseWheelDown = false;
+ boolean eventAlt = false;
+ boolean eventCtrl = false;
+ boolean eventShift = false;
// System.err.printf("buttons: %04x\r\n", buttons);
- switch (buttons) {
+ switch (buttons & 0xE3) {
case 0:
eventMouse1 = true;
mouse1 = true;
eventType = TMouseEvent.Type.MOUSE_MOTION;
break;
}
+
+ if ((buttons & 0x04) != 0) {
+ eventShift = true;
+ }
+ if ((buttons & 0x08) != 0) {
+ eventAlt = true;
+ }
+ if ((buttons & 0x10) != 0) {
+ eventCtrl = true;
+ }
+
return new TMouseEvent(eventType, x, y, x, y,
eventMouse1, eventMouse2, eventMouse3,
- eventMouseWheelUp, eventMouseWheelDown);
+ eventMouseWheelUp, eventMouseWheelDown,
+ eventAlt, eventCtrl, eventShift);
}
/**
boolean eventMouse3 = false;
boolean eventMouseWheelUp = false;
boolean eventMouseWheelDown = false;
+ boolean eventAlt = false;
+ boolean eventCtrl = false;
+ boolean eventShift = false;
if (release) {
eventType = TMouseEvent.Type.MOUSE_UP;
}
- switch (buttons) {
+ switch (buttons & 0xE3) {
case 0:
eventMouse1 = true;
break;
// Unknown, bail out
return null;
}
+
+ if ((buttons & 0x04) != 0) {
+ eventShift = true;
+ }
+ if ((buttons & 0x08) != 0) {
+ eventAlt = true;
+ }
+ if ((buttons & 0x10) != 0) {
+ eventCtrl = true;
+ }
+
return new TMouseEvent(eventType, x, y, x, y,
eventMouse1, eventMouse2, eventMouse3,
- eventMouseWheelUp, eventMouseWheelDown);
+ eventMouseWheelUp, eventMouseWheelDown,
+ eventAlt, eventCtrl, eventShift);
}
/**
if (decPrivateModeFlag == false) {
break;
}
+ boolean reportsJexerImages = false;
+ boolean reportsIterm2Images = false;
for (String x: params) {
if (x.equals("4")) {
// Terminal reports sixel support
if (debugToStderr) {
System.err.println("Device Attributes: Jexer images");
}
- jexerImages = true;
+ reportsJexerImages = true;
+ }
+ if (x.equals("1337")) {
+ // Terminal reports iTerm2 images support
+ if (debugToStderr) {
+ System.err.println("Device Attributes: iTerm2 images");
+ }
+ reportsIterm2Images = true;
}
}
+ if (reportsJexerImages == false) {
+ // Terminal does not support Jexer images, disable
+ // them.
+ jexerImageOption = JexerImageOption.DISABLED;
+ }
+ if (reportsIterm2Images == false) {
+ // Terminal does not support iTerm2 images, disable
+ // them.
+ iterm2Images = false;
+ }
+ resetParser();
return;
case 't':
// windowOps
* - enable sixel scrolling
*
* - disable private color registers (so that we can use one common
- * palette)
+ * palette) if sixelSharedPalette is set
*
* @return the string to emit to xterm
*/
private String xtermSetSixelSettings() {
- return "\033[?80h\033[?1070l";
+ if (sixelSharedPalette == true) {
+ return "\033[?80h\033[?1070l";
+ } else {
+ return "\033[?80h\033[?1070h";
+ }
}
/**
if (palette == null) {
palette = new SixelPalette();
- // TODO: make this an option (shared palette or not)
- palette.emitPalette(sb, null);
+ if (sixelSharedPalette == true) {
+ palette.emitPalette(sb, null);
+ }
}
return sb.toString();
if (y == height - 1) {
// We are on the bottom row. If scrolling mode is enabled
// (default), then VT320/xterm will scroll the entire screen if
- // we draw any pixels here.
-
- // TODO: support sixel scrolling mode disabled as an option.
+ // we draw any pixels here. Do not draw the image, bail out
+ // instead.
sb.append(normal());
sb.append(gotoXY(x, y));
for (int j = 0; j < cells.size(); j++) {
// System.err.println("CACHE MISS");
}
- int imageWidth = cells.get(0).getImage().getWidth();
- int imageHeight = cells.get(0).getImage().getHeight();
-
- // cells.get(x).getImage() has a dithered bitmap containing indexes
- // into the color palette. Piece these together into one larger
- // image for final rendering.
- int totalWidth = 0;
- int fullWidth = cells.size() * getTextWidth();
- int fullHeight = getTextHeight();
- for (int i = 0; i < cells.size(); i++) {
- totalWidth += cells.get(i).getImage().getWidth();
- }
-
- BufferedImage image = new BufferedImage(fullWidth,
- fullHeight, BufferedImage.TYPE_INT_ARGB);
-
- int [] rgbArray;
- for (int i = 0; i < cells.size() - 1; i++) {
- int tileWidth = Math.min(cells.get(i).getImage().getWidth(),
- imageWidth);
- int tileHeight = Math.min(cells.get(i).getImage().getHeight(),
- imageHeight);
-
- if (false && cells.get(i).isInvertedImage()) {
- // I used to put an all-white cell over the cursor, don't do
- // that anymore.
- rgbArray = new int[imageWidth * imageHeight];
- for (int j = 0; j < rgbArray.length; j++) {
- rgbArray[j] = 0xFFFFFF;
- }
- } else {
- try {
- rgbArray = cells.get(i).getImage().getRGB(0, 0,
- tileWidth, tileHeight, null, 0, tileWidth);
- } catch (Exception e) {
- throw new RuntimeException("image " + imageWidth + "x" +
- imageHeight +
- "tile " + tileWidth + "x" +
- tileHeight +
- " cells.get(i).getImage() " +
- cells.get(i).getImage() +
- " i " + i +
- " fullWidth " + fullWidth +
- " fullHeight " + fullHeight, e);
- }
- }
-
- /*
- System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
- i * imageWidth, 0, imageWidth, imageHeight,
- 0, imageWidth);
- System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
- fullWidth, fullHeight, cells.size(), getTextWidth());
- */
-
- image.setRGB(i * imageWidth, 0, tileWidth, tileHeight,
- rgbArray, 0, tileWidth);
- if (tileHeight < fullHeight) {
- int backgroundColor = cells.get(i).getBackground().getRGB();
- for (int imageX = 0; imageX < image.getWidth(); imageX++) {
- for (int imageY = imageHeight; imageY < fullHeight;
- imageY++) {
-
- image.setRGB(imageX, imageY, backgroundColor);
- }
- }
- }
- }
- totalWidth -= ((cells.size() - 1) * imageWidth);
- if (false && cells.get(cells.size() - 1).isInvertedImage()) {
- // I used to put an all-white cell over the cursor, don't do that
- // anymore.
- rgbArray = new int[totalWidth * imageHeight];
- for (int j = 0; j < rgbArray.length; j++) {
- rgbArray[j] = 0xFFFFFF;
- }
- } else {
- try {
- rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0,
- totalWidth, imageHeight, null, 0, totalWidth);
- } catch (Exception e) {
- throw new RuntimeException("image " + imageWidth + "x" +
- imageHeight + " cells.get(cells.size() - 1).getImage() " +
- cells.get(cells.size() - 1).getImage(), e);
- }
- }
- image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
- imageHeight, rgbArray, 0, totalWidth);
-
- if (totalWidth < getTextWidth()) {
- int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
-
- for (int imageX = image.getWidth() - totalWidth;
- imageX < image.getWidth(); imageX++) {
-
- for (int imageY = 0; imageY < fullHeight; imageY++) {
- image.setRGB(imageX, imageY, backgroundColor);
- }
- }
- }
+ BufferedImage image = cellsToImage(cells);
+ int fullHeight = image.getHeight();
// Dither the image. It is ok to lose the original here.
if (palette == null) {
palette = new SixelPalette();
- // TODO: make this an option (shared palette or not)
- palette.emitPalette(sb, null);
+ if (sixelSharedPalette == true) {
+ palette.emitPalette(sb, null);
+ }
}
image = palette.ditherImage(image);
int rasterHeight = 0;
int rasterWidth = image.getWidth();
- /*
-
- // TODO: make this an option (shared palette or not)
-
- // Emit the palette, but only for the colors actually used by these
- // cells.
- boolean [] usedColors = new boolean[sixelPaletteSize];
- for (int imageX = 0; imageX < image.getWidth(); imageX++) {
- for (int imageY = 0; imageY < image.getHeight(); imageY++) {
- usedColors[image.getRGB(imageX, imageY)] = true;
+ if (sixelSharedPalette == false) {
+ // Emit the palette, but only for the colors actually used by
+ // these cells.
+ boolean [] usedColors = new boolean[sixelPaletteSize];
+ for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+ for (int imageY = 0; imageY < image.getHeight(); imageY++) {
+ usedColors[image.getRGB(imageX, imageY)] = true;
+ }
}
+ palette.emitPalette(sb, usedColors);
}
- palette.emitPalette(sb, usedColors);
- */
// Render the entire row of cells.
for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) {
return sixel;
}
- // ------------------------------------------------------------------------
- // End sixel output support -----------------------------------------------
- // ------------------------------------------------------------------------
-
- // ------------------------------------------------------------------------
- // iTerm2 image output support --------------------------------------------
- // ------------------------------------------------------------------------
-
/**
- * Create an iTerm2 images string representing a row of several cells
- * containing bitmap data.
+ * Convert a horizontal range of cell's image data into a single
+ * contigous image, rescaled and anti-aliased to match the current text
+ * cell size.
*
- * @param x column coordinate. 0 is the left-most column.
- * @param y row coordinate. 0 is the top-most row.
- * @param cells the cells containing the bitmap data
- * @return the string to emit to an ANSI / ECMA-style terminal
+ * @param cells the cells containing image data
+ * @return the image resized to the current text cell size
*/
- private String toIterm2Image(final int x, final int y,
- final ArrayList<Cell> cells) {
-
- StringBuilder sb = new StringBuilder();
-
- assert (cells != null);
- assert (cells.size() > 0);
- assert (cells.get(0).getImage() != null);
-
- if (iterm2Images == false) {
- sb.append(normal());
- sb.append(gotoXY(x, y));
- for (int i = 0; i < cells.size(); i++) {
- sb.append(' ');
- }
- return sb.toString();
- }
-
- if (iterm2Cache == null) {
- iterm2Cache = new ImageCache(height * 10);
- base64 = java.util.Base64.getEncoder();
- }
-
- // Save and get rows to/from the cache that do NOT have inverted
- // cells.
- boolean saveInCache = true;
- for (Cell cell: cells) {
- if (cell.isInvertedImage()) {
- saveInCache = false;
- }
- }
- if (saveInCache) {
- String cachedResult = iterm2Cache.get(cells);
- if (cachedResult != null) {
- // System.err.println("CACHE HIT");
- sb.append(gotoXY(x, y));
- sb.append(cachedResult);
- return sb.toString();
- }
- // System.err.println("CACHE MISS");
- }
-
+ private BufferedImage cellsToImage(final List<Cell> cells) {
int imageWidth = cells.get(0).getImage().getWidth();
int imageHeight = cells.get(0).getImage().getHeight();
- // cells.get(x).getImage() has a dithered bitmap containing indexes
- // into the color palette. Piece these together into one larger
+ // Piece cells.get(x).getImage() pieces together into one larger
// image for final rendering.
int totalWidth = 0;
- int fullWidth = cells.size() * getTextWidth();
- int fullHeight = getTextHeight();
+ int fullWidth = cells.size() * imageWidth;
+ int fullHeight = imageHeight;
for (int i = 0; i < cells.size(); i++) {
totalWidth += cells.get(i).getImage().getWidth();
}
int [] rgbArray;
for (int i = 0; i < cells.size() - 1; i++) {
- int tileWidth = Math.min(cells.get(i).getImage().getWidth(),
- imageWidth);
- int tileHeight = Math.min(cells.get(i).getImage().getHeight(),
- imageHeight);
+ int tileWidth = imageWidth;
+ int tileHeight = imageHeight;
+
if (false && cells.get(i).isInvertedImage()) {
// I used to put an all-white cell over the cursor, don't do
// that anymore.
image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
imageHeight, rgbArray, 0, totalWidth);
- if (totalWidth < getTextWidth()) {
+ if (totalWidth < imageWidth) {
int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
for (int imageX = image.getWidth() - totalWidth;
}
}
+ if ((image.getWidth() != cells.size() * getTextWidth())
+ || (image.getHeight() != getTextHeight())
+ ) {
+ // Rescale the image to fit the text cells it is going into.
+ BufferedImage newImage;
+ newImage = new BufferedImage(cells.size() * getTextWidth(),
+ getTextHeight(), BufferedImage.TYPE_INT_ARGB);
+
+ Graphics gr = newImage.getGraphics();
+ if (gr instanceof Graphics2D) {
+ ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING,
+ RenderingHints.VALUE_RENDER_QUALITY);
+ }
+ gr.drawImage(image, 0, 0, newImage.getWidth(),
+ newImage.getHeight(), null, null);
+ gr.dispose();
+ image = newImage;
+ }
+
+ return image;
+ }
+
+ // ------------------------------------------------------------------------
+ // End sixel output support -----------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // iTerm2 image output support --------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Create an iTerm2 images string representing a row of several cells
+ * containing bitmap data.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param cells the cells containing the bitmap data
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String toIterm2Image(final int x, final int y,
+ final ArrayList<Cell> cells) {
+
+ StringBuilder sb = new StringBuilder();
+
+ assert (cells != null);
+ assert (cells.size() > 0);
+ assert (cells.get(0).getImage() != null);
+
+ if (iterm2Images == false) {
+ sb.append(normal());
+ sb.append(gotoXY(x, y));
+ for (int i = 0; i < cells.size(); i++) {
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+
+ if (iterm2Cache == null) {
+ iterm2Cache = new ImageCache(height * 10);
+ }
+
+ // Save and get rows to/from the cache that do NOT have inverted
+ // cells.
+ boolean saveInCache = true;
+ for (Cell cell: cells) {
+ if (cell.isInvertedImage()) {
+ saveInCache = false;
+ }
+ }
+ if (saveInCache) {
+ String cachedResult = iterm2Cache.get(cells);
+ if (cachedResult != null) {
+ // System.err.println("CACHE HIT");
+ sb.append(gotoXY(x, y));
+ sb.append(cachedResult);
+ return sb.toString();
+ }
+ // System.err.println("CACHE MISS");
+ }
+
+ BufferedImage image = cellsToImage(cells);
+ int fullHeight = image.getHeight();
+
/*
* From https://iterm2.com/documentation-images.html:
*
return "";
}
- // iTerm2 does not advance the cursor automatically, so place it
- // myself.
sb.append("\033]1337;File=");
/*
sb.append(String.format("width=$d;height=1;preserveAspectRatio=1;",
getTextHeight())));
*/
sb.append("inline=1:");
- sb.append(base64.encodeToString(pngOutputStream.toByteArray()));
+ sb.append(StringUtils.toBase64(pngOutputStream.toByteArray()));
sb.append("\007");
if (saveInCache) {
assert (cells.size() > 0);
assert (cells.get(0).getImage() != null);
- if (jexerImages == false) {
+ if (jexerImageOption == JexerImageOption.DISABLED) {
sb.append(normal());
sb.append(gotoXY(x, y));
for (int i = 0; i < cells.size(); i++) {
if (jexerCache == null) {
jexerCache = new ImageCache(height * 10);
- base64 = java.util.Base64.getEncoder();
}
// Save and get rows to/from the cache that do NOT have inverted
// System.err.println("CACHE MISS");
}
- int imageWidth = cells.get(0).getImage().getWidth();
- int imageHeight = cells.get(0).getImage().getHeight();
+ BufferedImage image = cellsToImage(cells);
+ int fullHeight = image.getHeight();
- // cells.get(x).getImage() has a dithered bitmap containing indexes
- // into the color palette. Piece these together into one larger
- // image for final rendering.
- int totalWidth = 0;
- int fullWidth = cells.size() * getTextWidth();
- int fullHeight = getTextHeight();
- for (int i = 0; i < cells.size(); i++) {
- totalWidth += cells.get(i).getImage().getWidth();
- }
-
- BufferedImage image = new BufferedImage(fullWidth,
- fullHeight, BufferedImage.TYPE_INT_ARGB);
-
- int [] rgbArray;
- for (int i = 0; i < cells.size() - 1; i++) {
- int tileWidth = Math.min(cells.get(i).getImage().getWidth(),
- imageWidth);
- int tileHeight = Math.min(cells.get(i).getImage().getHeight(),
- imageHeight);
- if (false && cells.get(i).isInvertedImage()) {
- // I used to put an all-white cell over the cursor, don't do
- // that anymore.
- rgbArray = new int[imageWidth * imageHeight];
- for (int j = 0; j < rgbArray.length; j++) {
- rgbArray[j] = 0xFFFFFF;
- }
- } else {
- try {
- rgbArray = cells.get(i).getImage().getRGB(0, 0,
- tileWidth, tileHeight, null, 0, tileWidth);
- } catch (Exception e) {
- throw new RuntimeException("image " + imageWidth + "x" +
- imageHeight +
- "tile " + tileWidth + "x" +
- tileHeight +
- " cells.get(i).getImage() " +
- cells.get(i).getImage() +
- " i " + i +
- " fullWidth " + fullWidth +
- " fullHeight " + fullHeight, e);
+ if (jexerImageOption == JexerImageOption.PNG) {
+ // Encode as PNG
+ ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream(1024);
+ try {
+ if (!ImageIO.write(image.getSubimage(0, 0, image.getWidth(),
+ Math.min(image.getHeight(), fullHeight)),
+ "PNG", pngOutputStream)
+ ) {
+ // We failed to render image, bail out.
+ return "";
}
+ } catch (IOException e) {
+ // We failed to render image, bail out.
+ return "";
}
- /*
- System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
- i * imageWidth, 0, imageWidth, imageHeight,
- 0, imageWidth);
- System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
- fullWidth, fullHeight, cells.size(), getTextWidth());
- */
+ sb.append("\033]444;1;0;");
+ sb.append(StringUtils.toBase64(pngOutputStream.toByteArray()));
+ sb.append("\007");
- image.setRGB(i * imageWidth, 0, tileWidth, tileHeight,
- rgbArray, 0, tileWidth);
- if (tileHeight < fullHeight) {
- int backgroundColor = cells.get(i).getBackground().getRGB();
- for (int imageX = 0; imageX < image.getWidth(); imageX++) {
- for (int imageY = imageHeight; imageY < fullHeight;
- imageY++) {
+ } else if (jexerImageOption == JexerImageOption.JPG) {
+
+ // Encode as JPG
+ ByteArrayOutputStream jpgOutputStream = new ByteArrayOutputStream(1024);
+
+ // Convert from ARGB to RGB, otherwise the JPG encode will fail.
+ BufferedImage jpgImage = new BufferedImage(image.getWidth(),
+ image.getHeight(), BufferedImage.TYPE_INT_RGB);
+ int [] pixels = new int[image.getWidth() * image.getHeight()];
+ image.getRGB(0, 0, image.getWidth(), image.getHeight(), pixels,
+ 0, image.getWidth());
+ jpgImage.setRGB(0, 0, image.getWidth(), image.getHeight(), pixels,
+ 0, image.getWidth());
- image.setRGB(imageX, imageY, backgroundColor);
- }
- }
- }
- }
- totalWidth -= ((cells.size() - 1) * imageWidth);
- if (false && cells.get(cells.size() - 1).isInvertedImage()) {
- // I used to put an all-white cell over the cursor, don't do that
- // anymore.
- rgbArray = new int[totalWidth * imageHeight];
- for (int j = 0; j < rgbArray.length; j++) {
- rgbArray[j] = 0xFFFFFF;
- }
- } else {
try {
- rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0,
- totalWidth, imageHeight, null, 0, totalWidth);
- } catch (Exception e) {
- throw new RuntimeException("image " + imageWidth + "x" +
- imageHeight + " cells.get(cells.size() - 1).getImage() " +
- cells.get(cells.size() - 1).getImage(), e);
+ if (!ImageIO.write(jpgImage.getSubimage(0, 0,
+ jpgImage.getWidth(),
+ Math.min(jpgImage.getHeight(), fullHeight)),
+ "JPG", jpgOutputStream)
+ ) {
+ // We failed to render image, bail out.
+ return "";
+ }
+ } catch (IOException e) {
+ // We failed to render image, bail out.
+ return "";
}
- }
- image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
- imageHeight, rgbArray, 0, totalWidth);
- if (totalWidth < getTextWidth()) {
- int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
+ sb.append("\033]444;2;0;");
+ sb.append(StringUtils.toBase64(jpgOutputStream.toByteArray()));
+ sb.append("\007");
- for (int imageX = image.getWidth() - totalWidth;
- imageX < image.getWidth(); imageX++) {
+ } else if (jexerImageOption == JexerImageOption.RGB) {
- for (int imageY = 0; imageY < fullHeight; imageY++) {
- image.setRGB(imageX, imageY, backgroundColor);
- }
- }
- }
+ // RGB
+ sb.append(String.format("\033]444;0;%d;%d;0;", image.getWidth(),
+ Math.min(image.getHeight(), fullHeight)));
- sb.append(String.format("\033]444;%d;%d;0;", image.getWidth(),
- Math.min(image.getHeight(), fullHeight)));
-
- byte [] bytes = new byte[image.getWidth() * image.getHeight() * 3];
- int stride = image.getWidth();
- for (int px = 0; px < stride; px++) {
- for (int py = 0; py < image.getHeight(); py++) {
- int rgb = image.getRGB(px, py);
- bytes[(py * stride * 3) + (px * 3)] = (byte) ((rgb >>> 16) & 0xFF);
- bytes[(py * stride * 3) + (px * 3) + 1] = (byte) ((rgb >>> 8) & 0xFF);
- bytes[(py * stride * 3) + (px * 3) + 2] = (byte) ( rgb & 0xFF);
+ byte [] bytes = new byte[image.getWidth() * image.getHeight() * 3];
+ int stride = image.getWidth();
+ for (int px = 0; px < stride; px++) {
+ for (int py = 0; py < image.getHeight(); py++) {
+ int rgb = image.getRGB(px, py);
+ bytes[(py * stride * 3) + (px * 3)] = (byte) ((rgb >>> 16) & 0xFF);
+ bytes[(py * stride * 3) + (px * 3) + 1] = (byte) ((rgb >>> 8) & 0xFF);
+ bytes[(py * stride * 3) + (px * 3) + 2] = (byte) ( rgb & 0xFF);
+ }
}
+ sb.append(StringUtils.toBase64(bytes));
+ sb.append("\007");
}
- sb.append(base64.encodeToString(bytes));
- sb.append("\007");
if (saveInCache) {
// This row is OK to save into the cache.
* @return true if this terminal is emitting Jexer images
*/
public boolean hasJexerImages() {
- return jexerImages;
+ return (jexerImageOption != JexerImageOption.DISABLED);
}
// ------------------------------------------------------------------------
package jexer.backend;
import java.awt.Font;
+import java.awt.FontFormatException;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
if (filename.length() == 0) {
// Fallback font
- font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2);
return;
}
ClassLoader loader = Thread.currentThread().getContextClassLoader();
InputStream in = loader.getResourceAsStream(filename);
fontRoot = Font.createFont(Font.TRUETYPE_FONT, in);
- font = fontRoot.deriveFont(Font.PLAIN, fontSize);
- } catch (java.awt.FontFormatException e) {
+ font = fontRoot.deriveFont(Font.PLAIN, fontSize - 2);
+ } catch (FontFormatException e) {
// Ideally we would report an error here, either via System.err
// or TExceptionDialog. However, I do not want GlyphMaker to
// know about available backends, so we quietly fallback to
// whatever is available as MONO.
- font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
- } catch (java.io.IOException e) {
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2);
+ } catch (IOException e) {
// See comment above.
- font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2);
}
}
import jexer.backend.GlyphMaker;
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
import jexer.bits.GraphicsChars;
import jexer.bits.StringUtils;
putFullwidthCharXY(x, y, cell);
}
+ /**
+ * Invert the cell color at a position, including both halves of a
+ * double-width cell.
+ *
+ * @param x column position
+ * @param y row position
+ */
+ public void invertCell(final int x, final int y) {
+ invertCell(x, y, false);
+ }
+
+ /**
+ * Invert the cell color at a position.
+ *
+ * @param x column position
+ * @param y row position
+ * @param onlyThisCell if true, only invert this cell, otherwise invert
+ * both halves of a double-width cell if necessary
+ */
+ public void invertCell(final int x, final int y,
+ final boolean onlyThisCell) {
+
+ Cell cell = getCharXY(x, y);
+ if (cell.isImage()) {
+ cell.invertImage();
+ }
+ if (cell.getForeColorRGB() < 0) {
+ cell.setForeColor(cell.getForeColor().invert());
+ } else {
+ cell.setForeColorRGB(cell.getForeColorRGB() ^ 0x00ffffff);
+ }
+ if (cell.getBackColorRGB() < 0) {
+ cell.setBackColor(cell.getBackColor().invert());
+ } else {
+ cell.setBackColorRGB(cell.getBackColorRGB() ^ 0x00ffffff);
+ }
+ putCharXY(x, y, cell);
+ if ((onlyThisCell == true) || (cell.getWidth() == Cell.Width.SINGLE)) {
+ return;
+ }
+
+ // This cell is one half of a fullwidth glyph. Invert the other
+ // half.
+ if (cell.getWidth() == Cell.Width.LEFT) {
+ if (x < width - 1) {
+ Cell rightHalf = getCharXY(x + 1, y);
+ if (rightHalf.getWidth() == Cell.Width.RIGHT) {
+ invertCell(x + 1, y, true);
+ return;
+ }
+ }
+ }
+ if (cell.getWidth() == Cell.Width.RIGHT) {
+ if (x > 0) {
+ Cell leftHalf = getCharXY(x - 1, y);
+ if (leftHalf.getWidth() == Cell.Width.LEFT) {
+ invertCell(x - 1, y, true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Set a selection area on the screen.
+ *
+ * @param x0 the starting X position of the selection
+ * @param y0 the starting Y position of the selection
+ * @param x1 the ending X position of the selection
+ * @param y1 the ending Y position of the selection
+ * @param rectangle if true, this is a rectangle select
+ */
+ public void setSelection(final int x0, final int y0,
+ final int x1, final int y1, final boolean rectangle) {
+
+ int startX = x0;
+ int startY = y0;
+ int endX = x1;
+ int endY = y1;
+
+ if (((x1 < x0) && (y1 == y0))
+ || (y1 < y0)
+ ) {
+ // The user dragged from bottom-to-top and/or right-to-left.
+ // Reverse the coordinates for the inverted section.
+ startX = x1;
+ startY = y1;
+ endX = x0;
+ endY = y0;
+ }
+ if (rectangle) {
+ for (int y = startY; y <= endY; y++) {
+ for (int x = startX; x <= endX; x++) {
+ invertCell(x, y);
+ }
+ }
+ } else {
+ if (endY > startY) {
+ for (int x = startX; x < width; x++) {
+ invertCell(x, startY);
+ }
+ for (int y = startY + 1; y < endY; y++) {
+ for (int x = 0; x < width; x++) {
+ invertCell(x, y);
+ }
+ }
+ for (int x = 0; x <= endX; x++) {
+ invertCell(x, endY);
+ }
+ } else {
+ assert (startY == endY);
+ for (int x = startX; x <= endX; x++) {
+ invertCell(x, startY);
+ }
+ }
+ }
+ }
+
+ /**
+ * Copy the screen selection area to the clipboard.
+ *
+ * @param clipboard the clipboard to use
+ * @param x0 the starting X position of the selection
+ * @param y0 the starting Y position of the selection
+ * @param x1 the ending X position of the selection
+ * @param y1 the ending Y position of the selection
+ * @param rectangle if true, this is a rectangle select
+ */
+ public void copySelection(final Clipboard clipboard,
+ final int x0, final int y0, final int x1, final int y1,
+ final boolean rectangle) {
+
+ StringBuilder sb = new StringBuilder();
+
+ int startX = x0;
+ int startY = y0;
+ int endX = x1;
+ int endY = y1;
+
+ if (((x1 < x0) && (y1 == y0))
+ || (y1 < y0)
+ ) {
+ // The user dragged from bottom-to-top and/or right-to-left.
+ // Reverse the coordinates for the inverted section.
+ startX = x1;
+ startY = y1;
+ endX = x0;
+ endY = y0;
+ }
+ if (rectangle) {
+ for (int y = startY; y <= endY; y++) {
+ for (int x = startX; x <= endX; x++) {
+ sb.append(Character.toChars(getCharXY(x, y).getChar()));
+ }
+ sb.append("\n");
+ }
+ } else {
+ if (endY > startY) {
+ for (int x = startX; x < width; x++) {
+ sb.append(Character.toChars(getCharXY(x, startY).getChar()));
+ }
+ sb.append("\n");
+ for (int y = startY + 1; y < endY; y++) {
+ for (int x = 0; x < width; x++) {
+ sb.append(Character.toChars(getCharXY(x, y).getChar()));
+ }
+ sb.append("\n");
+ }
+ for (int x = 0; x <= endX; x++) {
+ sb.append(Character.toChars(getCharXY(x, endY).getChar()));
+ }
+ } else {
+ assert (startY == endY);
+ for (int x = startX; x <= endX; x++) {
+ sb.append(Character.toChars(getCharXY(x, startY).getChar()));
+ }
+ }
+ }
+ clipboard.copyText(sb.toString());
+ }
+
}
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
/**
* MultiScreen mirrors its I/O to several screens.
* @return drawing boundary
*/
public int getClipRight() {
- return screens.get(0).getClipRight();
+ if (screens.size() > 0) {
+ return screens.get(0).getClipRight();
+ }
+ return 0;
}
/**
* @return drawing boundary
*/
public int getClipBottom() {
- return screens.get(0).getClipBottom();
+ if (screens.size() > 0) {
+ return screens.get(0).getClipBottom();
+ }
+ return 0;
}
/**
* @return drawing boundary
*/
public int getClipLeft() {
- return screens.get(0).getClipLeft();
+ if (screens.size() > 0) {
+ return screens.get(0).getClipLeft();
+ }
+ return 0;
}
/**
* @return drawing boundary
*/
public int getClipTop() {
- return screens.get(0).getClipTop();
+ if (screens.size() > 0) {
+ return screens.get(0).getClipTop();
+ }
+ return 0;
}
/**
* @return attributes at (x, y)
*/
public CellAttributes getAttrXY(final int x, final int y) {
- return screens.get(0).getAttrXY(x, y);
+ if (screens.size() > 0) {
+ return screens.get(0).getAttrXY(x, y);
+ }
+ return new CellAttributes();
}
/**
* @return the character + attributes
*/
public Cell getCharXY(final int x, final int y) {
- return screens.get(0).getCharXY(x, y);
+ if (screens.size() > 0) {
+ return screens.get(0).getCharXY(x, y);
+ }
+ return new Cell();
}
/**
*/
public int getHeight() {
// Return the smallest height of the screens.
- int height = screens.get(0).getHeight();
+ int height = 25;
+ if (screens.size() > 0) {
+ height = screens.get(0).getHeight();
+ }
for (Screen screen: screens) {
if (screen.getHeight() < height) {
height = screen.getHeight();
*/
public int getWidth() {
// Return the smallest width of the screens.
- int width = screens.get(0).getWidth();
+ int width = 80;
+ if (screens.size() > 0) {
+ width = screens.get(0).getWidth();
+ }
for (Screen screen: screens) {
if (screen.getWidth() < width) {
width = screen.getWidth();
* @return true if the cursor is visible
*/
public boolean isCursorVisible() {
- return screens.get(0).isCursorVisible();
+ if (screens.size() > 0) {
+ return screens.get(0).isCursorVisible();
+ }
+ return true;
}
/**
* @return the cursor x column position
*/
public int getCursorX() {
- return screens.get(0).getCursorX();
+ if (screens.size() > 0) {
+ return screens.get(0).getCursorX();
+ }
+ return 0;
}
/**
* @return the cursor y row position
*/
public int getCursorY() {
- return screens.get(0).getCursorY();
+ if (screens.size() > 0) {
+ return screens.get(0).getCursorY();
+ }
+ return 0;
}
/**
return textHeight;
}
+ /**
+ * Invert the cell color at a position, including both halves of a
+ * double-width cell.
+ *
+ * @param x column position
+ * @param y row position
+ */
+ public void invertCell(final int x, final int y) {
+ for (Screen screen: screens) {
+ screen.invertCell(x, y);
+ }
+ }
+
+ /**
+ * Invert the cell color at a position.
+ *
+ * @param x column position
+ * @param y row position
+ * @param onlyThisCell if true, only invert this cell, otherwise invert
+ * both halves of a double-width cell if necessary
+ */
+ public void invertCell(final int x, final int y,
+ final boolean onlyThisCell) {
+
+ for (Screen screen: screens) {
+ screen.invertCell(x, y, onlyThisCell);
+ }
+ }
+
+ /**
+ * Set a selection area on the screen.
+ *
+ * @param x0 the starting X position of the selection
+ * @param y0 the starting Y position of the selection
+ * @param x1 the ending X position of the selection
+ * @param y1 the ending Y position of the selection
+ * @param rectangle if true, this is a rectangle select
+ */
+ public void setSelection(final int x0, final int y0,
+ final int x1, final int y1, final boolean rectangle) {
+
+ for (Screen screen: screens) {
+ screen.setSelection(x0, y0, x1, y1, rectangle);
+ }
+ }
+
+ /**
+ * Copy the screen selection area to the clipboard.
+ *
+ * @param clipboard the clipboard to use
+ * @param x0 the starting X position of the selection
+ * @param y0 the starting Y position of the selection
+ * @param x1 the ending X position of the selection
+ * @param y1 the ending Y position of the selection
+ * @param rectangle if true, this is a rectangle select
+ */
+ public void copySelection(final Clipboard clipboard,
+ final int x0, final int y0, final int x1, final int y1,
+ final boolean rectangle) {
+
+ // Only copy from the first screen.
+ if (screens.size() > 0) {
+ screens.get(0).copySelection(clipboard, x0, y0, x1, y1, rectangle);
+ }
+ }
+
}
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
/**
* Drawing operations API.
*/
public int getTextHeight();
+ /**
+ * Invert the cell color at a position, including both halves of a
+ * double-width cell.
+ *
+ * @param x column position
+ * @param y row position
+ */
+ public void invertCell(final int x, final int y);
+
+ /**
+ * Invert the cell color at a position.
+ *
+ * @param x column position
+ * @param y row position
+ * @param onlyThisCell if true, only invert this cell, otherwise invert
+ * both halves of a double-width cell if necessary
+ */
+ public void invertCell(final int x, final int y,
+ final boolean onlyThisCell);
+
+ /**
+ * Set a selection area on the screen.
+ *
+ * @param x0 the starting X position of the selection
+ * @param y0 the starting Y position of the selection
+ * @param x1 the ending X position of the selection
+ * @param y1 the ending Y position of the selection
+ * @param rectangle if true, this is a rectangle select
+ */
+ public void setSelection(final int x0, final int y0,
+ final int x1, final int y1, final boolean rectangle);
+
+ /**
+ * Copy the screen selection area to the clipboard.
+ *
+ * @param clipboard the clipboard to use
+ * @param x0 the starting X position of the selection
+ * @param y0 the starting Y position of the selection
+ * @param x1 the ending X position of the selection
+ * @param y1 the ending Y position of the selection
+ * @param rectangle if true, this is a rectangle select
+ */
+ public void copySelection(final Clipboard clipboard,
+ final int x0, final int y0, final int x1, final int y1,
+ final boolean rectangle);
+
}
* Adjustable Insets for this component. This has the effect of adding a
* black border around the drawing area.
*/
- Insets adjustInsets = new Insets(BORDER + 5, BORDER, BORDER, BORDER);
+ Insets adjustInsets = null;
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
*/
public SwingComponent(final JFrame frame) {
this.frame = frame;
+ if (System.getProperty("os.name").startsWith("Linux")) {
+ // On my Linux dev system, a Swing frame draws its contents just
+ // a little off. No idea why, but I've seen it on both Debian
+ // and Fedora with KDE. These adjustments to the adjustments
+ // seem to center it OK in the frame.
+ adjustInsets = new Insets(BORDER + 5, BORDER,
+ BORDER - 3, BORDER + 2);
+ } else {
+ adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER);
+ }
setupFrame();
}
*/
public SwingComponent(final JComponent component) {
this.component = component;
+ adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER);
setupComponent();
}
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
+import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
) {
do {
do {
- clearPhysical();
drawToSwing();
} while (swing.getBufferStrategy().contentsRestored());
swing.getBufferStrategy().show();
Toolkit.getDefaultToolkit().sync();
} while (swing.getBufferStrategy().contentsLost());
-
} else {
// Non-triple-buffered, call drawToSwing() once
drawToSwing();
// Draw the background rectangle, then the foreground character.
assert (cell.isImage());
+
+ // Enable anti-aliasing
+ if (gr instanceof Graphics2D) {
+ ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING,
+ RenderingHints.VALUE_RENDER_QUALITY);
+ }
+
gr.setColor(cell.getBackground());
gr.fillRect(xPixel, yPixel, textWidth, textHeight);
BufferedImage image = cell.getImage();
if (image != null) {
if (swing.getFrame() != null) {
- gr.drawImage(image, xPixel, yPixel, swing.getFrame());
+ gr.drawImage(image, xPixel, yPixel, getTextWidth(),
+ getTextHeight(), swing.getFrame());
} else {
- gr.drawImage(image, xPixel, yPixel, swing.getComponent());
+ gr.drawImage(image, xPixel, yPixel, getTextWidth(),
+ getTextHeight(),swing.getComponent());
}
return;
}
cellColor.setBackColor(cell.getForeColor());
}
+ // Enable anti-aliasing
+ if ((gr instanceof Graphics2D) && (swing.getFrame() != null)) {
+ // Anti-aliasing on JComponent makes the hash character disappear
+ // for Terminus font, and also kills performance. Only enable it
+ // for JFrame.
+ ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING,
+ RenderingHints.VALUE_RENDER_QUALITY);
+ }
+
// Draw the background rectangle, then the foreground character.
gr2.setColor(attrToBackgroundColor(cellColor));
gr2.fillRect(gr2x, gr2y, textWidth, textHeight);
} else {
ch = key.getKeyChar();
}
- alt = key.isAltDown();
+ // Both meta and alt count as alt, thanks to Mac using alt for
+ // "symbols" so meta ("command") is the only other modifier left.
+ alt = key.isAltDown() | key.isMetaDown();
ctrl = key.isControlDown();
shift = key.isShiftDown();
/*
System.err.printf("Swing Key: %s\n", key);
System.err.printf(" isKey: %s\n", isKey);
+ System.err.printf(" meta: %s\n", key.isMetaDown());
System.err.printf(" alt: %s\n", alt);
System.err.printf(" ctrl: %s\n", ctrl);
System.err.printf(" shift: %s\n", shift);
boolean eventMouse1 = false;
boolean eventMouse2 = false;
boolean eventMouse3 = false;
+ boolean eventAlt = false;
+ boolean eventCtrl = false;
+ boolean eventShift = false;
+
if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
eventMouse1 = true;
}
if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
eventMouse3 = true;
}
+ if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) {
+ eventAlt = true;
+ }
+ if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) {
+ eventCtrl = true;
+ }
+ if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) {
+ eventShift = true;
+ }
+
mouse1 = eventMouse1;
mouse2 = eventMouse2;
mouse3 = eventMouse3;
int y = textRow(mouse.getY());
TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION,
- x, y, x, y, mouse1, mouse2, mouse3, false, false);
+ x, y, x, y, mouse1, mouse2, mouse3, false, false,
+ eventAlt, eventCtrl, eventShift);
synchronized (eventQueue) {
eventQueue.add(mouseEvent);
oldMouseX = x;
oldMouseY = y;
+ boolean eventAlt = false;
+ boolean eventCtrl = false;
+ boolean eventShift = false;
+
+ int modifiers = mouse.getModifiersEx();
+ if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) {
+ eventAlt = true;
+ }
+ if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) {
+ eventCtrl = true;
+ }
+ if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) {
+ eventShift = true;
+ }
+
TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION,
- x, y, x, y, mouse1, mouse2, mouse3, false, false);
+ x, y, x, y, mouse1, mouse2, mouse3, false, false,
+ eventAlt, eventCtrl, eventShift);
synchronized (eventQueue) {
eventQueue.add(mouseEvent);
boolean eventMouse1 = false;
boolean eventMouse2 = false;
boolean eventMouse3 = false;
+ boolean eventAlt = false;
+ boolean eventCtrl = false;
+ boolean eventShift = false;
+
if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
eventMouse1 = true;
}
if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
eventMouse3 = true;
}
+ if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) {
+ eventAlt = true;
+ }
+ if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) {
+ eventCtrl = true;
+ }
+ if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) {
+ eventShift = true;
+ }
+
mouse1 = eventMouse1;
mouse2 = eventMouse2;
mouse3 = eventMouse3;
int y = textRow(mouse.getY());
TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN,
- x, y, x, y, mouse1, mouse2, mouse3, false, false);
+ x, y, x, y, mouse1, mouse2, mouse3, false, false,
+ eventAlt, eventCtrl, eventShift);
synchronized (eventQueue) {
eventQueue.add(mouseEvent);
boolean eventMouse1 = false;
boolean eventMouse2 = false;
boolean eventMouse3 = false;
+ boolean eventAlt = false;
+ boolean eventCtrl = false;
+ boolean eventShift = false;
+
if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
eventMouse1 = true;
}
if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
eventMouse3 = true;
}
+ if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) {
+ eventAlt = true;
+ }
+ if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) {
+ eventCtrl = true;
+ }
+ if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) {
+ eventShift = true;
+ }
+
if (mouse1) {
mouse1 = false;
eventMouse1 = true;
int y = textRow(mouse.getY());
TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_UP,
- x, y, x, y, eventMouse1, eventMouse2, eventMouse3, false, false);
+ x, y, x, y, eventMouse1, eventMouse2, eventMouse3, false, false,
+ eventAlt, eventCtrl, eventShift);
synchronized (eventQueue) {
eventQueue.add(mouseEvent);
boolean eventMouse3 = false;
boolean mouseWheelUp = false;
boolean mouseWheelDown = false;
+ boolean eventAlt = false;
+ boolean eventCtrl = false;
+ boolean eventShift = false;
+
if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
eventMouse1 = true;
}
if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
eventMouse3 = true;
}
+ if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0) {
+ eventAlt = true;
+ }
+ if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) {
+ eventCtrl = true;
+ }
+ if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) {
+ eventShift = true;
+ }
+
mouse1 = eventMouse1;
mouse2 = eventMouse2;
mouse3 = eventMouse3;
}
TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN,
- x, y, x, y, mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown);
+ x, y, x, y, mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown,
+ eventAlt, eventCtrl, eventShift);
synchronized (eventQueue) {
eventQueue.add(mouseEvent);
int B = 23;
int hash = A;
hash = (B * hash) + super.hashCode();
- hash = (B * hash) + (int)ch;
+ hash = (B * hash) + ch;
hash = (B * hash) + width.hashCode();
if (image != null) {
/*
*/
private static final int PROTECT = 0x10;
-
// ------------------------------------------------------------------------
// Variables --------------------------------------------------------------
// ------------------------------------------------------------------------
return;
}
- while (token.equals("bold") || token.equals("blink")) {
- if (token.equals("bold")) {
+ while (token.equals("bold")
+ || token.equals("bright")
+ || token.equals("blink")
+ ) {
+ if (token.equals("bold") || token.equals("bright")) {
bold = true;
token = tokenizer.nextToken();
}
// Invalid line.
continue;
}
- String key = line.substring(0, line.indexOf(':')).trim();
- String text = line.substring(line.indexOf(':') + 1);
+ String key = line.substring(0, line.indexOf('=')).trim();
+ String text = line.substring(line.indexOf('=') + 1);
setColorFromString(key, text);
}
// All done.
color.setBackColor(Color.BLUE);
color.setBold(false);
colors.put("teditor", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.CYAN);
+ color.setBold(false);
+ colors.put("teditor.selected", color);
// TTable
color = new CellAttributes();
color.setBold(false);
colors.put("tsplitpane", color);
+ // THelpWindow border - during window movement
+ color = new CellAttributes();
+ color.setForeColor(Color.GREEN);
+ color.setBackColor(Color.CYAN);
+ color.setBold(true);
+ colors.put("thelpwindow.windowmove", color);
+
+ // THelpWindow border
+ color = new CellAttributes();
+ color.setForeColor(Color.GREEN);
+ color.setBackColor(Color.CYAN);
+ color.setBold(true);
+ colors.put("thelpwindow.border", color);
+
+ // THelpWindow background
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.CYAN);
+ color.setBold(true);
+ colors.put("thelpwindow.background", color);
+
+ // THelpWindow text
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("thelpwindow.text", color);
+
+ // THelpWindow link
+ color = new CellAttributes();
+ color.setForeColor(Color.YELLOW);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ colors.put("thelpwindow.link", color);
+
+ // THelpWindow link - active
+ color = new CellAttributes();
+ color.setForeColor(Color.YELLOW);
+ color.setBackColor(Color.CYAN);
+ color.setBold(true);
+ colors.put("thelpwindow.link.active", color);
+
}
/**
import java.util.List;
import java.util.ArrayList;
+import java.util.Arrays;
/**
* StringUtils contains methods to:
*
* - Read/write a line of RFC4180 comma-separated values strings to/from a
* list of strings.
+ *
+ * - Compute number of visible text cells for a given Unicode codepoint or
+ * string.
+ *
+ * - Convert bytes to and from base-64 encoding.
*/
public class StringUtils {
* @return the number of text cell columns required to display this string
*/
public static int width(final String str) {
+ if (str == null) {
+ return 0;
+ }
+
int n = 0;
for (int i = 0; i < str.length();) {
int ch = str.codePointAt(i);
return ((ch >= 0x1f004) && (ch <= 0x1fffd));
}
+ // ------------------------------------------------------------------------
+ // Base64 -----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /*
+ * The Base64 encoder/decoder below is provided to support JDK 1.6 - JDK
+ * 11. It was taken from https://sourceforge.net/projects/migbase64/
+ *
+ * The following changes were made:
+ *
+ * - Code has been indented and long lines cut to fit within 80 columns.
+ *
+ * - Char, String, and "fast" byte functions removed. byte versions
+ * retained and called toBase64()/fromBase64().
+ *
+ * - Enclosing braces added to blocks.
+ */
+
+ /**
+ * A very fast and memory efficient class to encode and decode to and
+ * from BASE64 in full accordance with RFC 2045.<br><br> On Windows XP
+ * sp1 with 1.4.2_04 and later ;), this encoder and decoder is about 10
+ * times faster on small arrays (10 - 1000 bytes) and 2-3 times as fast
+ * on larger arrays (10000 - 1000000 bytes) compared to
+ * <code>sun.misc.Encoder()/Decoder()</code>.<br><br>
+ *
+ * On byte arrays the encoder is about 20% faster than Jakarta Commons
+ * Base64 Codec for encode and about 50% faster for decoding large
+ * arrays. This implementation is about twice as fast on very small
+ * arrays (< 30 bytes). If source/destination is a <code>String</code>
+ * this version is about three times as fast due to the fact that the
+ * Commons Codec result has to be recoded to a <code>String</code> from
+ * <code>byte[]</code>, which is very expensive.<br><br>
+ *
+ * This encode/decode algorithm doesn't create any temporary arrays as
+ * many other codecs do, it only allocates the resulting array. This
+ * produces less garbage and it is possible to handle arrays twice as
+ * large as algorithms that create a temporary array. (E.g. Jakarta
+ * Commons Codec). It is unknown whether Sun's
+ * <code>sun.misc.Encoder()/Decoder()</code> produce temporary arrays but
+ * since performance is quite low it probably does.<br><br>
+ *
+ * The encoder produces the same output as the Sun one except that the
+ * Sun's encoder appends a trailing line separator if the last character
+ * isn't a pad. Unclear why but it only adds to the length and is
+ * probably a side effect. Both are in conformance with RFC 2045
+ * though.<br> Commons codec seem to always att a trailing line
+ * separator.<br><br>
+ *
+ * <b>Note!</b> The encode/decode method pairs (types) come in three
+ * versions with the <b>exact</b> same algorithm and thus a lot of code
+ * redundancy. This is to not create any temporary arrays for transcoding
+ * to/from different format types. The methods not used can simply be
+ * commented out.<br><br>
+ *
+ * There is also a "fast" version of all decode methods that works the
+ * same way as the normal ones, but har a few demands on the decoded
+ * input. Normally though, these fast verions should be used if the
+ * source if the input is known and it hasn't bee tampered with.<br><br>
+ *
+ * If you find the code useful or you find a bug, please send me a note
+ * at base64 @ miginfocom . com.
+ *
+ * Licence (BSD):
+ * ==============
+ *
+ * Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (base64 @ miginfocom
+ * . com) All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met: Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the MiG InfoCom AB nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @version 2.2
+ * @author Mikael Grev
+ * Date: 2004-aug-02
+ * Time: 11:31:11
+ */
+
+ private static final char[] CA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
+ private static final int[] IA = new int[256];
+ static {
+ Arrays.fill(IA, -1);
+ for (int i = 0, iS = CA.length; i < iS; i++) {
+ IA[CA[i]] = i;
+ }
+ IA['='] = 0;
+ }
+
+ /**
+ * Encodes a raw byte array into a BASE64 <code>byte[]</code>
+ * representation i accordance with RFC 2045.
+ * @param sArr The bytes to convert. If <code>null</code> or length 0
+ * an empty array will be returned.
+ * @return A BASE64 encoded array. Never <code>null</code>.
+ */
+ public final static String toBase64(byte[] sArr) {
+ // Check special case
+ int sLen = sArr != null ? sArr.length : 0;
+ if (sLen == 0) {
+ return "";
+ }
+
+ final boolean lineSep = true;
+
+ int eLen = (sLen / 3) * 3; // Length of even 24-bits.
+ int cCnt = ((sLen - 1) / 3 + 1) << 2; // Returned character count
+ int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0); // Length of returned array
+ byte[] dArr = new byte[dLen];
+
+ // Encode even 24-bits
+ for (int s = 0, d = 0, cc = 0; s < eLen;) {
+ // Copy next three bytes into lower 24 bits of int, paying
+ // attension to sign.
+ int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff);
+
+ // Encode the int into four chars
+ dArr[d++] = (byte) CA[(i >>> 18) & 0x3f];
+ dArr[d++] = (byte) CA[(i >>> 12) & 0x3f];
+ dArr[d++] = (byte) CA[(i >>> 6) & 0x3f];
+ dArr[d++] = (byte) CA[i & 0x3f];
+
+ // Add optional line separator
+ if (lineSep && ++cc == 19 && d < dLen - 2) {
+ dArr[d++] = '\r';
+ dArr[d++] = '\n';
+ cc = 0;
+ }
+ }
+
+ // Pad and encode last bits if source isn't an even 24 bits.
+ int left = sLen - eLen; // 0 - 2.
+ if (left > 0) {
+ // Prepare the int
+ int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0);
+
+ // Set last four chars
+ dArr[dLen - 4] = (byte) CA[i >> 12];
+ dArr[dLen - 3] = (byte) CA[(i >>> 6) & 0x3f];
+ dArr[dLen - 2] = left == 2 ? (byte) CA[i & 0x3f] : (byte) '=';
+ dArr[dLen - 1] = '=';
+ }
+ try {
+ return new String(dArr, "UTF-8");
+ } catch (java.io.UnsupportedEncodingException e) {
+ throw new IllegalArgumentException(e);
+ }
+
+ }
+
+ /**
+ * Decodes a BASE64 encoded byte array. All illegal characters will
+ * be ignored and can handle both arrays with and without line
+ * separators.
+ * @param sArr The source array. Length 0 will return an empty
+ * array. <code>null</code> will throw an exception.
+ * @return The decoded array of bytes. May be of length 0. Will be
+ * <code>null</code> if the legal characters (including '=') isn't
+ * divideable by 4. (I.e. definitely corrupted).
+ */
+ public final static byte[] fromBase64(byte[] sArr) {
+ // Check special case
+ int sLen = sArr.length;
+
+ // Count illegal characters (including '\r', '\n') to know what
+ // size the returned array will be, so we don't have to
+ // reallocate & copy it later.
+ int sepCnt = 0; // Number of separator characters. (Actually illegal characters, but that's a bonus...)
+ for (int i = 0; i < sLen; i++) {
+ // If input is "pure" (I.e. no line separators or illegal chars)
+ // base64 this loop can be commented out.
+ if (IA[sArr[i] & 0xff] < 0) {
+ sepCnt++;
+ }
+ }
+
+ // Check so that legal chars (including '=') are evenly
+ // divideable by 4 as specified in RFC 2045.
+ if ((sLen - sepCnt) % 4 != 0) {
+ return null;
+ }
+
+ int pad = 0;
+ for (int i = sLen; i > 1 && IA[sArr[--i] & 0xff] <= 0;) {
+ if (sArr[i] == '=') {
+ pad++;
+ }
+ }
+
+ int len = ((sLen - sepCnt) * 6 >> 3) - pad;
+
+ byte[] dArr = new byte[len]; // Preallocate byte[] of exact length
+
+ for (int s = 0, d = 0; d < len;) {
+ // Assemble three bytes into an int from four "valid" characters.
+ int i = 0;
+ for (int j = 0; j < 4; j++) { // j only increased if a valid char was found.
+ int c = IA[sArr[s++] & 0xff];
+ if (c >= 0) {
+ i |= c << (18 - j * 6);
+ } else {
+ j--;
+ }
+ }
+
+ // Add the bytes
+ dArr[d++] = (byte) (i >> 16);
+ if (d < len) {
+ dArr[d++]= (byte) (i >> 8);
+ if (d < len) {
+ dArr[d++] = (byte) i;
+ }
+ }
+ }
+
+ return dArr;
+ }
+
}
* 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
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"));
*/
private boolean mouseWheelDown;
+ /**
+ * Keyboard modifier ALT.
+ */
+ private boolean alt;
+
+ /**
+ * Keyboard modifier CTRL.
+ */
+ private boolean ctrl;
+
+ /**
+ * Keyboard modifier SHIFT.
+ */
+ private boolean shift;
+
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
* @param mouse3 if true, middle button is down
* @param mouseWheelUp if true, mouse wheel (button 4) is down
* @param mouseWheelDown if true, mouse wheel (button 5) is down
+ * @param alt if true, ALT was pressed with this mouse event
+ * @param ctrl if true, CTRL was pressed with this mouse event
+ * @param shift if true, SHIFT was pressed with this mouse event
*/
public TMouseEvent(final Type type, final int x, final int y,
final int absoluteX, final int absoluteY,
final boolean mouse1, final boolean mouse2, final boolean mouse3,
- final boolean mouseWheelUp, final boolean mouseWheelDown) {
+ final boolean mouseWheelUp, final boolean mouseWheelDown,
+ final boolean alt, final boolean ctrl, final boolean shift) {
this.type = type;
this.x = x;
this.mouse3 = mouse3;
this.mouseWheelUp = mouseWheelUp;
this.mouseWheelDown = mouseWheelDown;
+ this.alt = alt;
+ this.ctrl = ctrl;
+ this.shift = shift;
}
// ------------------------------------------------------------------------
return mouseWheelDown;
}
+ /**
+ * Getter for ALT.
+ *
+ * @return alt value
+ */
+ public boolean isAlt() {
+ return alt;
+ }
+
+ /**
+ * Getter for CTRL.
+ *
+ * @return ctrl value
+ */
+ public boolean isCtrl() {
+ return ctrl;
+ }
+
+ /**
+ * Getter for SHIFT.
+ *
+ * @return shift value
+ */
+ public boolean isShift() {
+ return shift;
+ }
+
/**
* Create a duplicate instance.
*
*/
public TMouseEvent dup() {
TMouseEvent mouse = new TMouseEvent(type, x, y, absoluteX, absoluteY,
- mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown);
+ mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown,
+ alt, ctrl, shift);
+
return mouse;
}
*/
@Override
public String toString() {
- return String.format("Mouse: %s x %d y %d absoluteX %d absoluteY %d 1 %s 2 %s 3 %s DOWN %s UP %s",
+ return String.format("Mouse: %s x %d y %d absoluteX %d absoluteY %d 1 %s 2 %s 3 %s DOWN %s UP %s ALT %s CTRL %s SHIFT %s",
type,
x, y,
absoluteX, absoluteY,
mouse2,
mouse3,
mouseWheelUp,
- mouseWheelDown);
+ mouseWheelDown,
+ alt, ctrl, shift);
}
}
--- /dev/null
+#!/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
+
if (timeoutMillis == 0) {
// Block on the read().
- return stream.read(b);
+ return stream.read(b, off, len);
}
int remaining = len;
*/
private void layoutChildren() {
double widthRatio = (double) width / originalWidth;
- if (!Double.isFinite(widthRatio)) {
+ if (Math.abs(widthRatio) > Double.MAX_VALUE) {
widthRatio = 1;
}
double heightRatio = (double) height / originalHeight;
- if (!Double.isFinite(heightRatio)) {
+ if (Math.abs(heightRatio) > Double.MAX_VALUE) {
heightRatio = 1;
}
for (TWidget child: children.keySet()) {
public static final int MID_SHELL = 13;
// Edit menu
- public static final int MID_CUT = 20;
- public static final int MID_COPY = 21;
- public static final int MID_PASTE = 22;
- public static final int MID_CLEAR = 23;
+ public static final int MID_UNDO = 20;
+ public static final int MID_REDO = 21;
+ public static final int MID_CUT = 22;
+ public static final int MID_COPY = 23;
+ public static final int MID_PASTE = 24;
+ public static final int MID_CLEAR = 25;
// Search menu
public static final int MID_FIND = 30;
*/
private MnemonicString mnemonic;
+ /**
+ * If true, draw icons with menu items. Note package private access.
+ */
+ boolean useIcons = false;
+
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
setHeight(2);
setActive(false);
+
+ if (System.getProperty("jexer.menuIcons", "false").equals("true")) {
+ useIcons = true;
+ }
+
}
// ------------------------------------------------------------------------
final boolean enabled) {
assert (id >= 1024);
- return addItemInternal(id, label, null, enabled);
+ return addItemInternal(id, label, null, enabled, -1);
}
/**
private TMenuItem addItemInternal(final int id, final String label,
final TKeypress key) {
- return addItemInternal(id, label, key, true);
+ return addItemInternal(id, label, key, true, -1);
}
/**
* @param label menu item label
* @param key global keyboard accelerator
* @param enabled default state for enabled
+ * @param icon icon picture/emoji
* @return the new menu item
*/
private TMenuItem addItemInternal(final int id, final String label,
- final TKeypress key, final boolean enabled) {
+ final TKeypress key, final boolean enabled, final int icon) {
int newY = getChildren().size() + 1;
assert (newY < getHeight());
- TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
+ TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label, icon);
menuItem.setKey(key);
menuItem.setEnabled(enabled);
setHeight(getHeight() + 1);
String label;
TKeypress key = null;
+ int icon = -1;
boolean checkable = false;
boolean checked = false;
case MID_REPAINT:
label = i18n.getString("menuRepaintDesktop");
+ icon = 0x1F3A8;
break;
case MID_VIEW_IMAGE:
case MID_NEW:
label = i18n.getString("menuNew");
+ icon = 0x1F5CE;
break;
case MID_EXIT:
label = i18n.getString("menuExit");
key = kbAltX;
+ icon = 0x1F5D9;
break;
case MID_SHELL:
label = i18n.getString("menuShell");
+ icon = 0x1F5AE;
break;
case MID_OPEN_FILE:
label = i18n.getString("menuOpen");
key = kbF3;
+ icon = 0x1F5C1;
break;
+ case MID_UNDO:
+ label = i18n.getString("menuUndo");
+ key = kbCtrlZ;
+ break;
+ case MID_REDO:
+ label = i18n.getString("menuRedo");
+ key = kbCtrlY;
+ break;
case MID_CUT:
label = i18n.getString("menuCut");
key = kbCtrlX;
+ icon = 0x1F5F6;
break;
case MID_COPY:
label = i18n.getString("menuCopy");
key = kbCtrlC;
+ icon = 0x1F5D0;
break;
case MID_PASTE:
label = i18n.getString("menuPaste");
key = kbCtrlV;
+ icon = 0x1F4CB;
break;
case MID_CLEAR:
label = i18n.getString("menuClear");
- // key = kbDel;
break;
case MID_FIND:
label = i18n.getString("menuFind");
+ icon = 0x1F50D;
break;
case MID_REPLACE:
label = i18n.getString("menuReplace");
break;
case MID_CASCADE:
label = i18n.getString("menuWindowCascade");
+ icon = 0x1F5D7;
break;
case MID_CLOSE_ALL:
label = i18n.getString("menuWindowCloseAll");
case MID_WINDOW_MOVE:
label = i18n.getString("menuWindowMove");
key = kbCtrlF5;
+ icon = 0x263C;
break;
case MID_WINDOW_ZOOM:
label = i18n.getString("menuWindowZoom");
key = kbF5;
+ icon = 0x2195;
break;
case MID_WINDOW_NEXT:
label = i18n.getString("menuWindowNext");
key = kbF6;
+ icon = 0x2192;
break;
case MID_WINDOW_PREVIOUS:
label = i18n.getString("menuWindowPrevious");
key = kbShiftF6;
+ icon = 0x2190;
break;
case MID_WINDOW_CLOSE:
label = i18n.getString("menuWindowClose");
throw new IllegalArgumentException("Invalid menu ID: " + id);
}
- TMenuItem item = addItemInternal(id, label, key, enabled);
+ TMenuItem item = addItemInternal(id, label, key, enabled, icon);
item.setCheckable(checkable);
return item;
}
menuExit=E&xit
menuShell=O&S Shell
menuOpen=&Open
+menuUndo=&Undo
+menuRedo=&Redo
menuCut=Cu&t
menuCopy=&Copy
menuPaste=&Paste
*/
private MnemonicString mnemonic;
+ /**
+ * An optional 2-cell-wide picture/icon for this item.
+ */
+ private int icon = -1;
+
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
TMenuItem(final TMenu parent, final int id, final int x, final int y,
final String label) {
+ this(parent, id, x, y, label, -1);
+ }
+
+ /**
+ * Package private constructor.
+ *
+ * @param parent parent widget
+ * @param id menu id
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param label menu item title
+ * @param icon icon picture/emoji
+ */
+ TMenuItem(final TMenu parent, final int id, final int x, final int y,
+ final String label, final int icon) {
+
// Set parent and window
super(parent);
setY(y);
setHeight(1);
this.label = mnemonic.getRawLabel();
- setWidth(StringUtils.width(label) + 4);
+ if (parent.useIcons) {
+ setWidth(StringUtils.width(label) + 6);
+ } else {
+ setWidth(StringUtils.width(label) + 4);
+ }
this.id = id;
+ this.icon = icon;
// Default state for some known menu items
switch (id) {
}
}
+ boolean useIcons = ((TMenu) getParent()).useIcons;
+
char cVSide = GraphicsChars.WINDOW_SIDE;
vLineXY(0, 0, 1, cVSide, background);
vLineXY(getWidth() - 1, 0, 1, cVSide, background);
hLineXY(1, 0, getWidth() - 2, ' ', menuColor);
- putStringXY(2, 0, mnemonic.getRawLabel(), menuColor);
+ putStringXY(2 + (useIcons ? 2 : 0), 0, mnemonic.getRawLabel(),
+ menuColor);
if (key != null) {
String keyLabel = key.toString();
putStringXY((getWidth() - StringUtils.width(keyLabel) - 2), 0,
keyLabel, menuColor);
}
if (mnemonic.getScreenShortcutIdx() >= 0) {
- putCharXY(2 + mnemonic.getScreenShortcutIdx(), 0,
- mnemonic.getShortcut(), menuMnemonicColor);
+ putCharXY(2 + (useIcons ? 2 : 0) + mnemonic.getScreenShortcutIdx(),
+ 0, mnemonic.getShortcut(), menuMnemonicColor);
}
if (checked) {
assert (checkable);
putCharXY(1, 0, GraphicsChars.CHECK, menuColor);
}
-
+ if ((useIcons == true) && (icon != -1)) {
+ putCharXY(2, 0, icon, menuColor);
+ }
}
// ------------------------------------------------------------------------
if (key != null) {
int newWidth = (StringUtils.width(label) + 4 +
StringUtils.width(key.toString()) + 2);
+ if (((TMenu) getParent()).useIcons) {
+ newWidth += 2;
+ }
if (newWidth > getWidth()) {
setWidth(newWidth);
}
}
}
+ /**
+ * Get a picture/emoji icon for this menu item.
+ *
+ * @return the codepoint, or -1 if no icon is specified for this menu
+ * item
+ */
+ public final int getIcon() {
+ return icon;
+ }
+
+ /**
+ * Set a picture/emoji icon for this menu item.
+ *
+ * @param icon a codepoint, or -1 to unset the icon
+ */
+ public final void setIcon(final int icon) {
+ this.icon = icon;
+ }
+
/**
* Dispatch event(s) due to selection or click.
*/
return menu.addItem(id, label, key);
}
+ /**
+ * Convenience function to add a custom menu item.
+ *
+ * @param id menu item ID. Must be greater than 1024.
+ * @param label menu item label
+ * @param key global keyboard accelerator
+ * @param enabled default state for enabled
+ * @return the new menu item
+ */
+ public TMenuItem addItem(final int id, final String label,
+ final TKeypress key, final boolean enabled) {
+
+ return menu.addItem(id, label, key, enabled);
+ }
+
/**
* Convenience function to add a menu item.
*
return menu.addItem(id, label);
}
+ /**
+ * Convenience function to add a menu item.
+ *
+ * @param id menu item ID. Must be greater than 1024.
+ * @param label menu item label
+ * @param enabled default state for enabled
+ * @return the new menu item
+ */
+ public TMenuItem addItem(final int id, final String label,
+ final boolean enabled) {
+
+ return menu.addItem(id, label, enabled);
+ }
+
/**
* Convenience function to add one of the default menu items.
*
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+
+<help>
+ <name>Jexer Help File</name>
+ <author>Kevin Lamonte</author>
+ <version>1.0.0</version>
+ <date>Jan 1, 2020</date>
+ <topics>
+ <topic title="Help">
+ <text>
+ This [window](Windows) does not have a specific help topic.
+ See [here](Help On Help) for general information on using the
+ help system.
+ </text>
+ </topic>
+ <topic title="Help On Help">
+ <text>
+ The #{help} system...
+
+ </text>
+ </topic>
+
+ <topic title="Menus">
+ <text>
+ #{Menus} do ...
+ </text>
+ </topic>
+
+ <topic title="Windows">
+ <text>
+ #{Windows} do ...
+ </text>
+ </topic>
+
+ <topic title="Editing Text">
+ <text>
+ The #{text editing} [window](Windows)...
+ </text>
+ </topic>
+
+ <topic title="Editing Tables">
+ <text></text>
+ </topic>
+
+ <topic title="Terminal Window">
+ <text>
+ The terminal window ...
+ </text>
+ </topic>
+
+ <topic title="Copyright Infomation">
+ <text>
+ Copyright (C) 2019 Kevin Lamonte
+
+ Available to all under the MIT License.
+ </text>
+ </topic>
+
+ </topics>
+</help>
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+/**
+ * EditMenuUser is used by TApplication to enable/disable edit menu items. A
+ * widget that supports these functions should define an onCommand method
+ * that operates on cmCut, cmCopy, cmPaste, and cmClear.
+ */
+public interface EditMenuUser {
+
+ /**
+ * Check if the cut menu item should be enabled.
+ *
+ * @return true if the cut menu item should be enabled
+ */
+ public boolean isEditMenuCut();
+
+ /**
+ * Check if the copy menu item should be enabled.
+ *
+ * @return true if the copy menu item should be enabled
+ */
+ public boolean isEditMenuCopy();
+
+ /**
+ * Check if the paste menu item should be enabled.
+ *
+ * @return true if the paste menu item should be enabled
+ */
+ public boolean isEditMenuPaste();
+
+ /**
+ * Check if the clear menu item should be enabled.
+ *
+ * @return true if the clear menu item should be enabled
+ */
+ public boolean isEditMenuClear();
+
+}
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+import java.util.ResourceBundle;
+
+import jexer.bits.CellAttributes;
+import jexer.event.TResizeEvent;
+import jexer.help.THelpText;
+import jexer.help.Topic;
+
+/**
+ * THelpWindow
+ */
+public class THelpWindow extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(THelpWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // Default help topic keys. Note package private access.
+ static String HELP_HELP = "Help On Help";
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The help text window.
+ */
+ private THelpText helpText;
+
+ /**
+ * The "Contents" button.
+ */
+ private TButton contentsButton;
+
+ /**
+ * The "Index" button.
+ */
+ private TButton indexButton;
+
+ /**
+ * The "Previous" button.
+ */
+ private TButton previousButton;
+
+ /**
+ * The "Close" button.
+ */
+ private TButton closeButton;
+
+ /**
+ * The X position for the buttons.
+ */
+ private int buttonOffset = 14;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param application TApplication that manages this window
+ * @param topic the topic to start on
+ */
+ public THelpWindow(final TApplication application, final String topic) {
+ this (application, application.helpFile.getTopic(topic));
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param application TApplication that manages this window
+ * @param topic the topic to start on
+ */
+ public THelpWindow(final TApplication application, final Topic topic) {
+ super(application, i18n.getString("windowTitle"),
+ 1, 1, 78, 22, CENTERED | RESIZABLE);
+
+ setMinimumWindowHeight(16);
+ setMinimumWindowWidth(30);
+
+ helpText = new THelpText(this, topic, 1, 1,
+ getWidth() - buttonOffset - 4, getHeight() - 4);
+
+ setHelpTopic(topic);
+
+ // Buttons
+ previousButton = addButton(i18n.getString("previousButton"),
+ getWidth() - buttonOffset, 4,
+ new TAction() {
+ public void DO() {
+ if (application.helpTopics.size() > 1) {
+ Topic previous = application.helpTopics.remove(
+ application.helpTopics.size() - 2);
+ application.helpTopics.remove(application.
+ helpTopics.size() - 1);
+ setHelpTopic(previous);
+ }
+ }
+ });
+
+ contentsButton = addButton(i18n.getString("contentsButton"),
+ getWidth() - buttonOffset, 6,
+ new TAction() {
+ public void DO() {
+ setHelpTopic(application.helpFile.getTableOfContents());
+ }
+ });
+
+ indexButton = addButton(i18n.getString("indexButton"),
+ getWidth() - buttonOffset, 8,
+ new TAction() {
+ public void DO() {
+ setHelpTopic(application.helpFile.getIndex());
+ }
+ });
+
+ closeButton = addButton(i18n.getString("closeButton"),
+ getWidth() - buttonOffset, 10,
+ new TAction() {
+ public void DO() {
+ // Don't copy anything, just close the window.
+ THelpWindow.this.close();
+ }
+ });
+
+ // Save this for last: make the close button default action.
+ activate(closeButton);
+
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param application TApplication that manages this window
+ */
+ public THelpWindow(final TApplication application) {
+ this(application, HELP_HELP);
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param event resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent event) {
+ if (event.getType() == TResizeEvent.Type.WIDGET) {
+
+ previousButton.setX(getWidth() - buttonOffset);
+ contentsButton.setX(getWidth() - buttonOffset);
+ indexButton.setX(getWidth() - buttonOffset);
+ closeButton.setX(getWidth() - buttonOffset);
+
+ helpText.setDimensions(1, 1, getWidth() - buttonOffset - 4,
+ getHeight() - 4);
+ helpText.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ helpText.getWidth(), helpText.getHeight()));
+
+ return;
+ } else {
+ super.onResize(event);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Retrieve the background color.
+ *
+ * @return the background color
+ */
+ @Override
+ public final CellAttributes getBackground() {
+ return getTheme().getColor("thelpwindow.background");
+ }
+
+ /**
+ * Retrieve the border color.
+ *
+ * @return the border color
+ */
+ @Override
+ public CellAttributes getBorder() {
+ if (inWindowMove) {
+ return getTheme().getColor("thelpwindow.windowmove");
+ }
+ return getTheme().getColor("thelpwindow.background");
+ }
+
+ /**
+ * Retrieve the color used by the window movement/sizing controls.
+ *
+ * @return the color used by the zoom box, resize bar, and close box
+ */
+ @Override
+ public CellAttributes getBorderControls() {
+ return getTheme().getColor("thelpwindow.border");
+ }
+
+ // ------------------------------------------------------------------------
+ // THelpWindow ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Set the topic to display.
+ *
+ * @param topic the topic to display
+ */
+ public void setHelpTopic(final String topic) {
+ setHelpTopic(getApplication().helpFile.getTopic(topic));
+ }
+
+ /**
+ * Set the topic to display.
+ *
+ * @param topic the topic to display
+ */
+ private void setHelpTopic(final Topic topic) {
+ boolean separator = true;
+ if ((topic == getApplication().helpFile.getTableOfContents())
+ || (topic == getApplication().helpFile.getIndex())
+ ) {
+ separator = false;
+ }
+
+ getApplication().helpTopics.add(topic);
+ helpText.setTopic(topic, separator);
+ }
+
+}
--- /dev/null
+windowTitle=Help
+previousButton=Pre&vious
+contentsButton=Co&ntents
+indexButton=\ &Index\ \
+closeButton=\ C&lose\ \
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.backend;
+
+import java.util.List;
+
+import jexer.event.TInputEvent;
+
+/**
+ * HeadlessBackend
+ */
+public class HeadlessBackend extends LogicalScreen implements Backend {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The session information.
+ */
+ private SessionInfo sessionInfo;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ */
+ public HeadlessBackend() {
+ sessionInfo = new TSessionInfo(width, height);
+ }
+
+ // ------------------------------------------------------------------------
+ // Backend ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Getter for sessionInfo.
+ *
+ * @return the SessionInfo
+ */
+ public final SessionInfo getSessionInfo() {
+ return sessionInfo;
+ }
+
+ /**
+ * Get a Screen, which displays the text cells to the user.
+ *
+ * @return the Screen
+ */
+ public Screen getScreen() {
+ return this;
+ }
+
+ /**
+ * Subclasses must provide an implementation that syncs the logical
+ * screen to the physical device.
+ */
+ public void flushScreen() {
+ // NOP
+ }
+
+ /**
+ * Check if there are events in the queue.
+ *
+ * @return if true, getEvents() has something to return to the application
+ */
+ public boolean hasEvents() {
+ return false;
+ }
+
+ /**
+ * Subclasses must provide an implementation to get keyboard, mouse, and
+ * screen resize events.
+ *
+ * @param queue list to append new events to
+ */
+ public void getEvents(List<TInputEvent> queue) {
+ // NOP
+ }
+
+ /**
+ * Subclasses must provide an implementation that closes sockets,
+ * restores console, etc.
+ */
+ public void shutdown() {
+ // NOP
+ }
+
+ /**
+ * Set listener to a different Object.
+ *
+ * @param listener the new listening object that run() wakes up on new
+ * input
+ */
+ public void setListener(final Object listener) {
+ // NOP
+ }
+
+ /**
+ * Reload backend options from System properties.
+ */
+ public void reloadOptions() {
+ // NOP
+ }
+
+}
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.bits;
+
+import java.awt.Image;
+import java.awt.Toolkit;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.StringSelection;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+
+/**
+ * Clipboard provides convenience methods to copy text and images to and from
+ * a shared clipboard. When the system clipboard is available it is used.
+ */
+public class Clipboard {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The image last copied to the clipboard.
+ */
+ private BufferedImage image = null;
+
+ /**
+ * The text string last copied to the clipboard.
+ */
+ private String text = null;
+
+ /**
+ * The system clipboard, or null if it is not available.
+ */
+ private java.awt.datatransfer.Clipboard systemClipboard = null;
+
+ /**
+ * The image selection class.
+ */
+ private ImageSelection imageSelection;
+
+ /**
+ * ImageSelection is used to hold an image while on the clipboard.
+ */
+ private class ImageSelection implements Transferable {
+
+ /**
+ * Returns an array of DataFlavor objects indicating the flavors the
+ * data can be provided in. The array should be ordered according to
+ * preference for providing the data (from most richly descriptive to
+ * least descriptive).
+ *
+ * @return an array of data flavors in which this data can be
+ * transferred
+ */
+ public DataFlavor[] getTransferDataFlavors() {
+ return new DataFlavor[] { DataFlavor.imageFlavor };
+ }
+
+ /**
+ * Returns whether or not the specified data flavor is supported for
+ * this object.
+ *
+ * @param flavor the requested flavor for the data
+ * @return boolean indicating whether or not the data flavor is
+ * supported
+ */
+ public boolean isDataFlavorSupported(DataFlavor flavor) {
+ return DataFlavor.imageFlavor.equals(flavor);
+ }
+
+ /**
+ * Returns an object which represents the data to be transferred. The
+ * class of the object returned is defined by the representation
+ * class of the flavor.
+ *
+ * @param flavor the requested flavor for the data
+ * @throws IOException if the data is no longer available in the
+ * requested flavor.
+ * @throws UnsupportedFlavorException if the requested data flavor is
+ * not supported.
+ */
+ public Object getTransferData(DataFlavor flavor)
+ throws UnsupportedFlavorException, IOException {
+
+ if (!DataFlavor.imageFlavor.equals(flavor)) {
+ throw new UnsupportedFlavorException(flavor);
+ }
+ return image;
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ */
+ public Clipboard() {
+ try {
+ systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ } catch (java.awt.HeadlessException e) {
+ // SQUASH
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Clipboard --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Copy an image to the clipboard.
+ *
+ * @param image image to copy
+ */
+ public void copyImage(final BufferedImage image) {
+ this.image = image;
+ if (systemClipboard != null) {
+ ImageSelection imageSelection = new ImageSelection();
+ systemClipboard.setContents(imageSelection, null);
+ }
+ }
+
+ /**
+ * Copy a text string to the clipboard.
+ *
+ * @param text string to copy
+ */
+ public void copyText(final String text) {
+ this.text = text;
+ if (systemClipboard != null) {
+ StringSelection stringSelection = new StringSelection(text);
+ systemClipboard.setContents(stringSelection, null);
+ }
+ }
+
+ /**
+ * Obtain an image from the clipboard.
+ *
+ * @return image from the clipboard, or null if no image is available
+ */
+ public BufferedImage pasteImage() {
+ if (systemClipboard != null) {
+ getClipboardImage();
+ }
+ return image;
+ }
+
+ /**
+ * Obtain a text string from the clipboard.
+ *
+ * @return text string from the clipboard, or null if no text is
+ * available
+ */
+ public String pasteText() {
+ if (systemClipboard != null) {
+ getClipboardText();
+ }
+ return text;
+ }
+
+ /**
+ * Returns true if the clipboard has an image.
+ *
+ * @return true if an image is available from the clipboard
+ */
+ public boolean isImage() {
+ if (image == null) {
+ getClipboardImage();
+ }
+ return (image != null);
+ }
+
+ /**
+ * Returns true if the clipboard has a text string.
+ *
+ * @return true if a text string is available from the clipboard
+ */
+ public boolean isText() {
+ if (text == null) {
+ getClipboardText();
+ }
+ return (text != null);
+ }
+
+ /**
+ * Returns true if the clipboard is empty.
+ *
+ * @return true if the clipboard is empty
+ */
+ public boolean isEmpty() {
+ return ((isText() == false) && (isImage() == false));
+ }
+
+ /**
+ * Copy image from the clipboard to text.
+ */
+ private void getClipboardImage() {
+ if (systemClipboard != null) {
+ Transferable contents = systemClipboard.getContents(null);
+ if (contents != null) {
+ if (contents.isDataFlavorSupported(DataFlavor.imageFlavor)) {
+ try {
+ Image img = (Image) contents.getTransferData(DataFlavor.imageFlavor);
+ image = new BufferedImage(img.getWidth(null),
+ img.getHeight(null), BufferedImage.TYPE_INT_ARGB);
+ image.getGraphics().drawImage(img, 0, 0, null);
+ } catch (IOException e) {
+ // SQUASH
+ } catch (UnsupportedFlavorException e) {
+ // SQUASH
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Copy text string from the clipboard to text.
+ */
+ private void getClipboardText() {
+ if (systemClipboard != null) {
+ Transferable contents = systemClipboard.getContents(null);
+ if (contents != null) {
+ if (contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
+ try {
+ text = (String) contents.getTransferData(DataFlavor.stringFlavor);
+ } catch (IOException e) {
+ // SQUASH
+ } catch (UnsupportedFlavorException e) {
+ // SQUASH
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Clear whatever is on the local clipboard. Note that this will not
+ * clear the system clipboard.
+ */
+ public void clear() {
+ image = null;
+ text = null;
+ }
+
+}
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.demos;
+
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.text.MessageFormat;
+import java.util.ResourceBundle;
+
+import jexer.TApplication;
+import jexer.backend.*;
+import jexer.demos.DemoApplication;
+import jexer.net.TelnetServerSocket;
+
+
+/**
+ * This class shows off the use of MultiBackend and MultiScreen.
+ */
+public class Demo8 {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(Demo8.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Demo8 ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Main entry point.
+ *
+ * @param args Command line arguments
+ */
+ public static void main(final String [] args) {
+ ServerSocket server = null;
+ try {
+
+ /*
+ * In this demo we will create a headless application that anyone
+ * can telnet to.
+ */
+
+ /*
+ * Check the arguments for the port to listen on.
+ */
+ if (args.length == 0) {
+ System.err.println(i18n.getString("usageString"));
+ return;
+ }
+ int port = Integer.parseInt(args[0]);
+
+ /*
+ * We create a headless screen and use it to establish a
+ * MultiBackend.
+ */
+ HeadlessBackend headlessBackend = new HeadlessBackend();
+ MultiBackend multiBackend = new MultiBackend(headlessBackend);
+
+ /*
+ * Now we create the shared application (a standard demo) and
+ * spin it up.
+ */
+ DemoApplication demoApp = new DemoApplication(multiBackend);
+ (new Thread(demoApp)).start();
+ multiBackend.setListener(demoApp);
+
+ /*
+ * Fire up the telnet server.
+ */
+ server = new TelnetServerSocket(port);
+ while (demoApp.isRunning()) {
+ Socket socket = server.accept();
+ System.out.println(MessageFormat.
+ format(i18n.getString("newConnection"), socket));
+
+ ECMA48Backend ecmaBackend = new ECMA48Backend(demoApp,
+ socket.getInputStream(),
+ socket.getOutputStream());
+
+ /*
+ * Add this screen to the MultiBackend, and at this point we
+ * have the telnet client able to use the shared demo
+ * application.
+ */
+ multiBackend.addBackend(ecmaBackend);
+
+ /*
+ * Emit the connection information from telnet.
+ */
+ Thread.sleep(500);
+ System.out.println(MessageFormat.
+ format(i18n.getString("terminal"),
+ ((jexer.net.TelnetInputStream) socket.getInputStream()).
+ getTerminalType()));
+ System.out.println(MessageFormat.
+ format(i18n.getString("username"),
+ ((jexer.net.TelnetInputStream) socket.getInputStream()).
+ getUsername()));
+ System.out.println(MessageFormat.
+ format(i18n.getString("language"),
+ ((jexer.net.TelnetInputStream) socket.getInputStream()).
+ getLanguage()));
+
+ } // while (demoApp.isRunning())
+
+ /*
+ * When the application exits, kill all of the connections too.
+ */
+ multiBackend.shutdown();
+ server.close();
+
+ System.out.println(i18n.getString("exitMain"));
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ if (server != null) {
+ try {
+ server.close();
+ } catch (Exception e) {
+ // SQUASH
+ }
+ }
+ }
+ }
+
+}
--- /dev/null
+usageString=USAGE: java -cp jexer.jar jexer.demos.Demo8 port
+newConnection=New connection: {0}
+username=\ \ \ username: {0}
+language=\ \ \ language: {0}
+terminal=\ \ \ terminal: {0}
+exitMain=Main thread is exiting...
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ResourceBundle;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.ParserConfigurationException;
+import org.xml.sax.SAXException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * A HelpFile is a collection of Topics with a table of contents and index of
+ * relevant terms.
+ */
+public class HelpFile {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(HelpFile.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The XML factory.
+ */
+ private static DocumentBuilder domBuilder;
+
+ /**
+ * The map of topics by title.
+ */
+ private HashMap<String, Topic> topicsByTitle;
+
+ /**
+ * The map of topics by index key term.
+ */
+ private HashMap<String, Topic> topicsByTerm;
+
+ /**
+ * The special "table of contents" topic.
+ */
+ private Topic tableOfContents;
+
+ /**
+ * The special "index" topic.
+ */
+ private Topic index;
+
+ /**
+ * The name of this help file.
+ */
+ private String name = "";
+
+ /**
+ * The version of this help file.
+ */
+ private String version = "";
+
+ /**
+ * The help file author.
+ */
+ private String author = "";
+
+ /**
+ * The help file copyright/written by date.
+ */
+ private String date = "";
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // HelpFile ---------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Load a help file from an input stream.
+ *
+ * @param input the input strem
+ * @throws IOException if an I/O error occurs
+ * @throws ParserConfigurationException if no XML parser is available
+ * @throws SAXException if XML parsing fails
+ */
+ public void load(final InputStream input) throws IOException,
+ ParserConfigurationException, SAXException {
+
+ topicsByTitle = new HashMap<String, Topic>();
+ topicsByTerm = new HashMap<String, Topic>();
+
+ try {
+ loadTopics(input);
+ } finally {
+ // Always generate the TOC and Index from what was read.
+ generateTableOfContents();
+ generateIndex();
+ }
+ }
+
+ /**
+ * Get a topic by title.
+ *
+ * @param title the title for the topic
+ * @return the topic, or the "not found" topic if title is not found
+ */
+ public Topic getTopic(final String title) {
+ Topic topic = topicsByTitle.get(title);
+ if (topic == null) {
+ return Topic.NOT_FOUND;
+ }
+ return topic;
+ }
+
+ /**
+ * Get the special "search results" topic.
+ *
+ * @param searchString a regular expression search string
+ * @return an index topic containing topics with text that matches the
+ * search string
+ */
+ public Topic getSearchResults(final String searchString) {
+ List<Topic> allTopics = new ArrayList<Topic>();
+ allTopics.addAll(topicsByTitle.values());
+ Collections.sort(allTopics);
+
+ List<Topic> results = new ArrayList<Topic>();
+ Pattern pattern = Pattern.compile(searchString);
+ Pattern patternLower = Pattern.compile(searchString.toLowerCase());
+
+ for (Topic topic: allTopics) {
+ Matcher match = pattern.matcher(topic.getText().toLowerCase());
+ if (match.find()) {
+ results.add(topic);
+ continue;
+ }
+ match = pattern.matcher(topic.getTitle().toLowerCase());
+ if (match.find()) {
+ results.add(topic);
+ continue;
+ }
+ match = patternLower.matcher(topic.getText().toLowerCase());
+ if (match.find()) {
+ results.add(topic);
+ continue;
+ }
+ match = patternLower.matcher(topic.getTitle().toLowerCase());
+ if (match.find()) {
+ results.add(topic);
+ continue;
+ }
+ }
+
+ StringBuilder text = new StringBuilder();
+ int wordIndex = 0;
+ List<Link> links = new ArrayList<Link>();
+ for (Topic topic: results) {
+ text.append(topic.getTitle());
+ text.append("\n\n");
+
+ Link link = new Link(topic.getTitle(), topic.getTitle(), wordIndex);
+ wordIndex += link.getWordCount();
+ links.add(link);
+ }
+
+ return new Topic(MessageFormat.format(i18n.getString("searchResults"),
+ searchString), text.toString(), links);
+ }
+
+ /**
+ * Get the special "table of contents" topic.
+ *
+ * @return the table of contents topic
+ */
+ public Topic getTableOfContents() {
+ return tableOfContents;
+ }
+
+ /**
+ * Get the special "index" topic.
+ *
+ * @return the index topic
+ */
+ public Topic getIndex() {
+ return index;
+ }
+
+ /**
+ * Generate the table of contents topic.
+ */
+ private void generateTableOfContents() {
+ List<Topic> allTopics = new ArrayList<Topic>();
+ allTopics.addAll(topicsByTitle.values());
+ Collections.sort(allTopics);
+
+ StringBuilder text = new StringBuilder();
+ int wordIndex = 0;
+ List<Link> links = new ArrayList<Link>();
+ for (Topic topic: allTopics) {
+ text.append(topic.getTitle());
+ text.append("\n\n");
+
+ Link link = new Link(topic.getTitle(), topic.getTitle(), wordIndex);
+ wordIndex += link.getWordCount();
+ links.add(link);
+ }
+
+ tableOfContents = new Topic(i18n.getString("tableOfContents"),
+ text.toString(), links);
+ }
+
+ /**
+ * Generate the index topic.
+ */
+ private void generateIndex() {
+ List<Topic> allTopics = new ArrayList<Topic>();
+ allTopics.addAll(topicsByTitle.values());
+
+ HashMap<String, ArrayList<Topic>> allKeys;
+ allKeys = new HashMap<String, ArrayList<Topic>>();
+ for (Topic topic: allTopics) {
+ for (String key: topic.getIndexKeys()) {
+ key = key.toLowerCase();
+ ArrayList<Topic> topics = allKeys.get(key);
+ if (topics == null) {
+ topics = new ArrayList<Topic>();
+ allKeys.put(key, topics);
+ }
+ topics.add(topic);
+ }
+ }
+ List<String> keys = new ArrayList<String>();
+ keys.addAll(allKeys.keySet());
+ Collections.sort(keys);
+
+ StringBuilder text = new StringBuilder();
+ int wordIndex = 0;
+ List<Link> links = new ArrayList<Link>();
+
+ for (String key: keys) {
+ List<Topic> topics = allKeys.get(key);
+ assert (topics != null);
+ for (Topic topic: topics) {
+ String line = String.format("%15s %15s", key, topic.getTitle());
+ text.append(line);
+ text.append("\n\n");
+
+ wordIndex += key.split("\\s+").length;
+ Link link = new Link(topic.getTitle(), topic.getTitle(), wordIndex);
+ wordIndex += link.getWordCount();
+ links.add(link);
+ }
+ }
+
+ index = new Topic(i18n.getString("index"), text.toString(), links);
+ }
+
+ /**
+ * Load topics from a help file into the topics pool.
+ *
+ * @param input the input strem
+ * @throws IOException if an I/O error occurs
+ * @throws ParserConfigurationException if no XML parser is available
+ * @throws SAXException if XML parsing fails
+ */
+ private void loadTopics(final InputStream input) throws IOException,
+ ParserConfigurationException, SAXException {
+
+ if (domBuilder == null) {
+ DocumentBuilderFactory dbFactory = DocumentBuilderFactory.
+ newInstance();
+ domBuilder = dbFactory.newDocumentBuilder();
+ }
+ Document doc = domBuilder.parse(input);
+
+ // Get the document's root XML node
+ Node root = doc.getChildNodes().item(0);
+ NodeList level1 = root.getChildNodes();
+ for (int i = 0; i < level1.getLength(); i++) {
+ Node node = level1.item(i);
+ String name = node.getNodeName();
+ String value = node.getTextContent();
+
+ if (name.equals("name")) {
+ this.name = value;
+ }
+ if (name.equals("version")) {
+ this.version = value;
+ }
+ if (name.equals("author")) {
+ this.author = value;
+ }
+ if (name.equals("date")) {
+ this.date = value;
+ }
+ if (name.equals("topics")) {
+ NodeList topics = node.getChildNodes();
+ for (int j = 0; j < topics.getLength(); j++) {
+ Node topic = topics.item(j);
+ addTopic(topic);
+ }
+ }
+ }
+ }
+
+ /**
+ * Add a topic to this help file.
+ *
+ * @param xmlNode the topic XML node
+ * @throws IOException if a java.io operation throws
+ */
+ private void addTopic(final Node xmlNode) throws IOException {
+ String title = "";
+ String text = "";
+
+ NamedNodeMap attributes = xmlNode.getAttributes();
+ if (attributes != null) {
+ for (int i = 0; i < attributes.getLength(); i++) {
+ Node attr = attributes.item(i);
+ if (attr.getNodeName().equals("title")) {
+ title = attr.getNodeValue().trim();
+ }
+ }
+ }
+ NodeList level2 = xmlNode.getChildNodes();
+ for (int i = 0; i < level2.getLength(); i++) {
+ Node node = level2.item(i);
+ String nodeName = node.getNodeName();
+ String nodeValue = node.getTextContent();
+ if (nodeName.equals("text")) {
+ text = nodeValue.trim();
+ }
+ }
+ if (title.length() > 0) {
+ Topic topic = new Topic(title, text);
+ topicsByTitle.put(title, topic);
+ }
+ }
+
+}
--- /dev/null
+tableOfContents=Table Of Contents
+index=Index
+searchResults=Search Results - {0}
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.ResourceBundle;
+
+/**
+ * A Link is a section of text with a reference to a Topic.
+ */
+public class Link {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The topic id that this link points to.
+ */
+ private String topic;
+
+ /**
+ * The text inside the link tag.
+ */
+ private String text;
+
+ /**
+ * The number of words in this link.
+ */
+ private int wordCount;
+
+ /**
+ * The word number (from the beginning of topic text) that corresponds to
+ * the first word of this link.
+ */
+ private int index;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param topic the topic to point to
+ * @param text the text inside the link tag
+ * @param index the word count index
+ */
+ public Link(final String topic, final String text, final int index) {
+ this.topic = topic;
+ this.text = text;
+ this.index = index;
+ this.wordCount = text.split("\\s+").length;
+ }
+
+ // ------------------------------------------------------------------------
+ // Link -------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the topic.
+ *
+ * @return the topic
+ */
+ public String getTopic() {
+ return topic;
+ }
+
+ /**
+ * Get the link text.
+ *
+ * @return the text inside the link tag
+ */
+ public String getText() {
+ return text;
+ }
+
+ /**
+ * Get the word index for this link.
+ *
+ * @return the word number (from the beginning of topic text) that
+ * corresponds to the first word of this link
+ */
+ public int getIndex() {
+ return index;
+ }
+
+ /**
+ * Get the number of words in this link.
+ *
+ * @return the number of words in this link
+ */
+ public int getWordCount() {
+ return wordCount;
+ }
+
+ /**
+ * Generate a human-readable string for this widget.
+ *
+ * @return a human-readable string
+ */
+ @Override
+ public String toString() {
+ return String.format("%s(%8x) topic %s link text %s word # %d count %d",
+ getClass().getName(), hashCode(), topic, text, index, wordCount);
+ }
+
+}
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jexer.THelpWindow;
+import jexer.TScrollableWidget;
+import jexer.TVScroller;
+import jexer.TWidget;
+import jexer.bits.CellAttributes;
+import jexer.bits.StringUtils;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
+import static jexer.TKeypress.*;
+
+/**
+ * THelpText displays help text with clickable links in a scrollable text
+ * area. It reflows automatically on resize.
+ */
+public class THelpText extends TScrollableWidget {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The number of lines to scroll on mouse wheel up/down.
+ */
+ private static final int wheelScrollSize = 3;
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The paragraphs in this text box.
+ */
+ private List<TParagraph> paragraphs;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param topic the topic to display
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ */
+ public THelpText(final THelpWindow parent, final Topic topic, final int x,
+ final int y, final int width, final int height) {
+
+ // Set parent and window
+ super(parent, x, y, width, height);
+
+ vScroller = new TVScroller(this, getWidth() - 1, 0,
+ Math.max(1, getHeight()));
+
+ setTopic(topic);
+ }
+
+ // ------------------------------------------------------------------------
+ // TScrollableWidget ------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Override TWidget's width: we need to set child widget widths.
+ *
+ * @param width new widget width
+ */
+ @Override
+ public void setWidth(final int width) {
+ super.setWidth(width);
+ if (hScroller != null) {
+ hScroller.setWidth(getWidth() - 1);
+ }
+ if (vScroller != null) {
+ vScroller.setX(getWidth() - 1);
+ }
+ }
+
+ /**
+ * Override TWidget's height: we need to set child widget heights.
+ * time.
+ *
+ * @param height new widget height
+ */
+ @Override
+ public void setHeight(final int height) {
+ super.setHeight(height);
+ if (hScroller != null) {
+ hScroller.setY(getHeight() - 1);
+ }
+ if (vScroller != null) {
+ vScroller.setHeight(Math.max(1, getHeight()));
+ }
+ }
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ // Pass to children
+ super.onMouseDown(mouse);
+
+ if (mouse.isMouseWheelUp()) {
+ for (int i = 0; i < wheelScrollSize; i++) {
+ vScroller.decrement();
+ }
+ reflowData();
+ return;
+ }
+ if (mouse.isMouseWheelDown()) {
+ for (int i = 0; i < wheelScrollSize; i++) {
+ vScroller.increment();
+ }
+ reflowData();
+ return;
+ }
+
+ // User clicked on a paragraph, update the scrollbar accordingly.
+ for (int i = 0; i < paragraphs.size(); i++) {
+ if (paragraphs.get(i).isActive()) {
+ setVerticalValue(i);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbTab)) {
+ getParent().switchWidget(true);
+ } else if (keypress.equals(kbShiftTab)) {
+ getParent().switchWidget(false);
+ } else if (keypress.equals(kbUp)) {
+ if (!paragraphs.get(getVerticalValue()).up()) {
+ vScroller.decrement();
+ reflowData();
+ }
+ } else if (keypress.equals(kbDown)) {
+ if (!paragraphs.get(getVerticalValue()).down()) {
+ vScroller.increment();
+ reflowData();
+ }
+ } else if (keypress.equals(kbPgUp)) {
+ vScroller.bigDecrement();
+ reflowData();
+ } else if (keypress.equals(kbPgDn)) {
+ vScroller.bigIncrement();
+ reflowData();
+ } else if (keypress.equals(kbHome)) {
+ vScroller.toTop();
+ reflowData();
+ } else if (keypress.equals(kbEnd)) {
+ vScroller.toBottom();
+ reflowData();
+ } else {
+ // Pass other keys on
+ super.onKeypress(keypress);
+ }
+ }
+
+ /**
+ * Place the scrollbars on the edge of this widget, and adjust bigChange
+ * to match the new size. This is called by onResize().
+ */
+ protected void placeScrollbars() {
+ if (hScroller != null) {
+ hScroller.setY(getHeight() - 1);
+ hScroller.setWidth(getWidth() - 1);
+ hScroller.setBigChange(getWidth() - 1);
+ }
+ if (vScroller != null) {
+ vScroller.setX(getWidth() - 1);
+ vScroller.setHeight(getHeight());
+ vScroller.setBigChange(getHeight());
+ }
+ }
+
+ /**
+ * Resize text and scrollbars for a new width/height.
+ */
+ @Override
+ public void reflowData() {
+ for (TParagraph paragraph: paragraphs) {
+ paragraph.setWidth(getWidth() - 1);
+ paragraph.reflowData();
+ }
+
+ int top = getVerticalValue();
+ int paragraphsHeight = 0;
+ for (TParagraph paragraph: paragraphs) {
+ paragraphsHeight += paragraph.getHeight();
+ }
+ if (paragraphsHeight <= getHeight()) {
+ // All paragraphs fit in the window.
+ int y = 0;
+ for (int i = 0; i < paragraphs.size(); i++) {
+ paragraphs.get(i).setEnabled(true);
+ paragraphs.get(i).setVisible(true);
+ paragraphs.get(i).setY(y);
+ y += paragraphs.get(i).getHeight();
+ }
+ activate(paragraphs.get(getVerticalValue()));
+ return;
+ }
+
+ /*
+ * Some paragraphs will not fit in the window. Find the number of
+ * rows needed to display from the current vertical position to the
+ * end:
+ *
+ * - If this meets or exceeds the available height, then draw from
+ * the vertical position to the number of visible rows.
+ *
+ * - If this is less than the available height, back up until
+ * meeting/exceeding the height, and draw from there to the end.
+ *
+ */
+ int rowsNeeded = 0;
+ for (int i = getVerticalValue(); i <= getBottomValue(); i++) {
+ rowsNeeded += paragraphs.get(i).getHeight();
+ }
+ while (rowsNeeded < getHeight()) {
+ // Decrease top until we meet/exceed the visible display.
+ if (top == getTopValue()) {
+ break;
+ }
+ top--;
+ rowsNeeded += paragraphs.get(top).getHeight();
+ }
+
+ // All set, now disable all paragraphs except the visible ones.
+ for (TParagraph paragraph: paragraphs) {
+ paragraph.setEnabled(false);
+ paragraph.setVisible(false);
+ paragraph.setY(-1);
+ }
+ int y = 0;
+ for (int i = top; (i <= getBottomValue()) && (y < getHeight()); i++) {
+ paragraphs.get(i).setEnabled(true);
+ paragraphs.get(i).setVisible(true);
+ paragraphs.get(i).setY(y);
+ y += paragraphs.get(i).getHeight();
+ }
+ activate(paragraphs.get(getVerticalValue()));
+ }
+
+ /**
+ * Draw the text box.
+ */
+ @Override
+ public void draw() {
+ // Setup my color
+ CellAttributes color = getTheme().getColor("thelpwindow.text");
+ for (int y = 0; y < getHeight(); y++) {
+ hLineXY(0, y, getWidth(), ' ', color);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // THelpText --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Set the topic.
+ *
+ * @param topic new topic to display
+ */
+ public void setTopic(final Topic topic) {
+ setTopic(topic, true);
+ }
+
+ /**
+ * Set the topic.
+ *
+ * @param topic new topic to display
+ * @param separator if true, separate paragraphs
+ */
+ public void setTopic(final Topic topic, final boolean separator) {
+
+ if (paragraphs != null) {
+ getChildren().removeAll(paragraphs);
+ }
+ paragraphs = new ArrayList<TParagraph>();
+
+ // Add title paragraph at top. We explicitly set the separator to
+ // false to achieve the underscore effect.
+ List<TWord> title = new ArrayList<TWord>();
+ title.add(new TWord(topic.getTitle(), null));
+ TParagraph titleParagraph = new TParagraph(this, title);
+ titleParagraph.separator = false;
+ paragraphs.add(titleParagraph);
+ title = new ArrayList<TWord>();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < topic.getTitle().length(); i++) {
+ sb.append('\u2580');
+ }
+ title.add(new TWord(sb.toString(), null));
+ titleParagraph = new TParagraph(this, title);
+ paragraphs.add(titleParagraph);
+
+ // Now add the actual text as paragraphs.
+ int wordIndex = 0;
+
+ // Break up text into paragraphs
+ String [] blocks = topic.getText().split("\n\n");
+ for (String block: blocks) {
+ List<TWord> words = new ArrayList<TWord>();
+ String [] lines = block.split("\n");
+ for (String line: lines) {
+ line = line.trim();
+ // System.err.println("line: " + line);
+ String [] wordTokens = line.split("\\s+");
+ for (int i = 0; i < wordTokens.length; i++) {
+ String wordStr = wordTokens[i].trim();
+ Link wordLink = null;
+ for (Link link: topic.getLinks()) {
+ if ((i + wordIndex >= link.getIndex())
+ && (i + wordIndex < link.getIndex() + link.getWordCount())
+ ) {
+ // This word is part of a link.
+ wordLink = link;
+ wordStr = link.getText();
+ i += link.getWordCount() - 1;
+ break;
+ }
+ }
+ TWord word = new TWord(wordStr, wordLink);
+ /*
+ System.err.println("add word at " + (i + wordIndex) + " : "
+ + wordStr + " " + wordLink);
+ */
+ words.add(word);
+ } // for (int i = 0; i < words.length; i++)
+ wordIndex += wordTokens.length;
+ } // for (String line: lines)
+ TParagraph paragraph = new TParagraph(this, words);
+ paragraph.separator = separator;
+ paragraphs.add(paragraph);
+ } // for (String block: blocks)
+
+ setBottomValue(paragraphs.size() - 1);
+ setVerticalValue(0);
+ reflowData();
+ }
+
+}
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.util.List;
+
+import jexer.TWidget;
+
+/**
+ * TParagraph contains a reflowable collection of TWords, some of which are
+ * clickable links.
+ */
+public class TParagraph extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Topic text and links converted to words.
+ */
+ private List<TWord> words;
+
+ /**
+ * If true, add one row to height as a paragraph separator. Note package
+ * private access.
+ */
+ boolean separator = true;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param words the pieces of the paragraph to display
+ */
+ public TParagraph(final THelpText parent, final List<TWord> words) {
+
+ // Set parent and window
+ super(parent, 0, 0, parent.getWidth() - 1, 1);
+
+ this.words = words;
+ for (TWord word: words) {
+ word.setParent(this, false);
+ }
+
+ reflowData();
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // TParagraph -------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Reposition the words in this paragraph to reflect the new width, and
+ * set the paragraph height.
+ */
+ public void reflowData() {
+ int x = 0;
+ int y = 0;
+ for (TWord word: words) {
+ if (x + word.getWidth() >= getWidth()) {
+ x = 0;
+ y++;
+ }
+ word.setX(x);
+ word.setY(y);
+ x += word.getWidth() + 1;
+ }
+ if (separator) {
+ setHeight(y + 2);
+ } else {
+ setHeight(y + 1);
+ }
+ }
+
+ /**
+ * Try to select a previous link.
+ *
+ * @return true if there was a previous link in this paragraph to select
+ */
+ public boolean up() {
+ if (words.size() == 0) {
+ return false;
+ }
+ if (getActiveChild() == this) {
+ // No selectable links
+ return false;
+ }
+ TWord firstWord = null;
+ TWord lastWord = null;
+ for (TWord word: words) {
+ if (word.isEnabled()) {
+ if (firstWord == null) {
+ firstWord = word;
+ }
+ lastWord = word;
+ }
+ }
+ if (getActiveChild() == firstWord) {
+ return false;
+ }
+ switchWidget(false);
+ return true;
+ }
+
+ /**
+ * Try to select a next link.
+ *
+ * @return true if there was a next link in this paragraph to select
+ */
+ public boolean down() {
+ if (words.size() == 0) {
+ return false;
+ }
+ if (getActiveChild() == this) {
+ // No selectable links
+ return false;
+ }
+ TWord firstWord = null;
+ TWord lastWord = null;
+ for (TWord word: words) {
+ if (word.isEnabled()) {
+ if (firstWord == null) {
+ firstWord = word;
+ }
+ lastWord = word;
+ }
+ }
+ if (getActiveChild() == lastWord) {
+ return false;
+ }
+ switchWidget(true);
+ return true;
+ }
+
+
+}
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import jexer.THelpWindow;
+import jexer.TWidget;
+import jexer.bits.CellAttributes;
+import jexer.bits.StringUtils;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
+import static jexer.TKeypress.*;
+
+/**
+ * TWord contains either a string to display or a clickable link.
+ */
+public class TWord extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The word(s) to display.
+ */
+ private String words;
+
+ /**
+ * Link to another Topic.
+ */
+ private Link link;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param words the words to display
+ * @param link link to other topic, or null
+ */
+ public TWord(final String words, final Link link) {
+
+ // TWord is created by THelpText before the TParagraph is belongs to
+ // is created, so pass null as parent for now.
+ super(null, 0, 0, StringUtils.width(words), 1);
+
+ this.words = words;
+ this.link = link;
+
+ // Don't make text-only words "active".
+ if (link == null) {
+ setEnabled(false);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (mouse.isMouse1()) {
+ if (link != null) {
+ ((THelpWindow) getWindow()).setHelpTopic(link.getTopic());
+ }
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbEnter)) {
+ if (link != null) {
+ ((THelpWindow) getWindow()).setHelpTopic(link.getTopic());
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the words.
+ */
+ @Override
+ public void draw() {
+ CellAttributes color = getTheme().getColor("thelpwindow.text");
+ if (link != null) {
+ if (isAbsoluteActive()) {
+ color = getTheme().getColor("thelpwindow.link.active");
+ } else {
+ color = getTheme().getColor("thelpwindow.link");
+ }
+ }
+ putStringXY(0, 0, words, color);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWord ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+}
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.ResourceBundle;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A Topic is a page of help text with a title and possibly links to other
+ * Topics.
+ */
+public class Topic implements Comparable<Topic> {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(Topic.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The "not found" topic to display when a key or index term does not
+ * have an associated topic. Note package private access.
+ */
+ static Topic NOT_FOUND = null;
+
+ /**
+ * The regex for identifying index tags.
+ */
+ private static final String INDEX_REGEX_STR = "\\#\\{([^\\}]*)\\}";
+
+ /**
+ * The regex for identifying link tags.
+ */
+ private static final String LINK_REGEX_STR = "\\[([^\\]]*)\\]\\(([^\\)]*)\\)";
+
+ /**
+ * The regex for identifying words.
+ */
+ private static final String WORD_REGEX_STR = "[ \\t]+";
+
+ /**
+ * The index match regex.
+ */
+ private static Pattern INDEX_REGEX;
+
+ /**
+ * The link match regex.
+ */
+ private static Pattern LINK_REGEX;
+
+ /**
+ * The word match regex.
+ */
+ private static Pattern WORD_REGEX;
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The title for this topic.
+ */
+ private String title;
+
+ /**
+ * The text for this topic.
+ */
+ private String text;
+
+ /**
+ * The index keys in this topic.
+ */
+ private Set<String> indexKeys = new HashSet<String>();
+
+ /**
+ * The links in this topic.
+ */
+ private List<Link> links = new ArrayList<Link>();
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Static constructor.
+ */
+ static {
+ try {
+ INDEX_REGEX = Pattern.compile(INDEX_REGEX_STR);
+ LINK_REGEX = Pattern.compile(LINK_REGEX_STR);
+ WORD_REGEX = Pattern.compile(WORD_REGEX_STR);
+
+ NOT_FOUND = new Topic(i18n.getString("topicNotFoundTitle"),
+ i18n.getString("topicNotFoundText"));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param title the topic title
+ * @param text the topic text
+ */
+ public Topic(final String title, final String text) {
+ this.title = title;
+ processText(text);
+ }
+
+ /**
+ * Package private constructor.
+ *
+ * @param title the topic title
+ * @param text the topic text
+ * @param links links to add after processing text
+ */
+ Topic(final String title, final String text, final List<Link> links) {
+ this.title = title;
+ processText(text);
+ this.links.addAll(links);
+ }
+
+ // ------------------------------------------------------------------------
+ // Topic ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the topic title.
+ *
+ * @return the title
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Get the topic text.
+ *
+ * @return the text
+ */
+ public String getText() {
+ return text;
+ }
+
+ /**
+ * Get the index keys.
+ *
+ * @return the keys
+ */
+ public Set<String> getIndexKeys() {
+ return indexKeys;
+ }
+
+ /**
+ * Get the links.
+ *
+ * @return the links
+ */
+ public List<Link> getLinks() {
+ return links;
+ }
+
+ /**
+ * Comparison operator.
+ *
+ * @param that another Topic instance
+ * @return comparison by topic title
+ */
+ public int compareTo(final Topic that) {
+ return title.compareTo(that.title);
+ }
+
+ /**
+ * Generate a human-readable string for this widget.
+ *
+ * @return a human-readable string
+ */
+ @Override
+ public String toString() {
+ return String.format("%s(%8x) topic %s text %s links %s indexKeys %s",
+ getClass().getName(), hashCode(), title, text, links, indexKeys);
+ }
+
+ /**
+ * Process a string through the regexes, building up the indexes and
+ * links.
+ *
+ * @param text the text to process
+ */
+ private void processText(final String text) {
+ StringBuilder sb = new StringBuilder();
+ String [] lines = text.split("\n");
+ int wordIndex = 0;
+ for (String line: lines) {
+ line = line.trim();
+
+ String cleanLine = "";
+
+ // System.err.println("LINE " + wordIndex + " : '" + line + "'");
+
+ Matcher index = INDEX_REGEX.matcher(line);
+ int start = 0;
+ while (index.find()) {
+ cleanLine += line.substring(start, index.start());
+ String key = index.group(1);
+ cleanLine += key;
+ start = index.end();
+ // System.err.println("ADD KEY: " + key);
+ indexKeys.add(key);
+ }
+ cleanLine += line.substring(start);
+
+ line = cleanLine;
+ cleanLine = "";
+
+ /*
+ System.err.println("line after removing #{index} tags: " +
+ wordIndex + " '" + line + "'");
+ */
+
+ Matcher link = LINK_REGEX.matcher(line);
+ start = 0;
+
+ boolean hasLink = link.find();
+
+ // System.err.println("hasLink " + hasLink);
+
+ while (true) {
+
+ if (hasLink == false) {
+ cleanLine += line.substring(start);
+
+ String remaining = line.substring(start).trim();
+ Matcher word = WORD_REGEX.matcher(remaining);
+ while (word.find()) {
+ // System.err.println("word.find() true");
+ wordIndex++;
+ }
+ if (remaining.length() > 0) {
+ // The last word on the line.
+ wordIndex++;
+ }
+ break;
+ }
+
+ assert (hasLink == true);
+
+ int linkWordIndex = link.start();
+ int cleanLineStart = cleanLine.length();
+ cleanLine += line.substring(start, linkWordIndex);
+ String linkText = link.group(1);
+ String topic = link.group(2);
+ cleanLine += linkText;
+ start = link.end();
+
+ // Increment wordIndex until we reach the first word of
+ // the link text.
+ Matcher word = WORD_REGEX.matcher(cleanLine.
+ substring(cleanLineStart));
+ while (word.find()) {
+ if (word.end() <= linkWordIndex) {
+ wordIndex++;
+ } else {
+ // We have found the word that matches the first
+ // word of link text, bail out.
+ break;
+ }
+ }
+ /*
+ System.err.println("ADD LINK --> " + topic + ": '" +
+ linkText + "' word index " + wordIndex);
+ */
+ links.add(new Link(topic, linkText, wordIndex));
+
+ // The rest of the words in the link text.
+ while (word.find()) {
+ wordIndex++;
+ }
+ // The final word after the last whitespace.
+ wordIndex++;
+
+ hasLink = link.find();
+ if (hasLink) {
+ wordIndex += 3;
+ }
+ }
+
+
+ /*
+ System.err.println("line after removing [link](...) tags: '" +
+ cleanLine + "'");
+ */
+
+ // Append the entire line.
+ sb.append(cleanLine);
+ sb.append("\n");
+
+ this.text = sb.toString();
+
+ } // for (String line: lines)
+
+ }
+
+}
--- /dev/null
+topicNotFoundTitle=Topic Not Found
+topicNotFoundText=The help topic was not found.
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+
+/**
+ * Online help system.
+ */
+package jexer.help;
*/
private Highlighter highlighter = new Highlighter();
+ /**
+ * The tab stop size.
+ */
+ private int tabSize = 8;
+
+ /**
+ * If true, backspace at an indent level goes back a full indent level.
+ * If false, backspace always goes back one column.
+ */
+ private boolean backspaceUnindents = false;
+
+ /**
+ * If true, save files with tab characters. If false, convert tabs to
+ * spaces when saving files.
+ */
+ private boolean saveWithTabs = false;
+
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
public Document(final String str, final CellAttributes defaultColor) {
this.defaultColor = defaultColor;
- // TODO: set different colors based on file extension
+ // Set colors to resemble the Borland IDE colors, but for Java
+ // language keywords.
highlighter.setJavaColors();
String [] rawLines = str.split("\n");
}
}
+ /**
+ * Private constructor used by dup().
+ */
+ private Document() {
+ // NOP
+ }
+
// ------------------------------------------------------------------------
// Document ---------------------------------------------------------------
// ------------------------------------------------------------------------
+ /**
+ * Create a duplicate instance.
+ *
+ * @return duplicate intance
+ */
+ public Document dup() {
+ Document other = new Document();
+ for (Line line: lines) {
+ other.lines.add(line.dup());
+ }
+ other.lineNumber = lineNumber;
+ other.overwrite = overwrite;
+ other.dirty = dirty;
+ other.defaultColor = defaultColor;
+ other.highlighter.setTo(highlighter);
+ return other;
+ }
+
/**
* Get the overwrite flag.
*
* @return true if addChar() overwrites data, false if it inserts
*/
- public boolean getOverwrite() {
+ public boolean isOverwrite() {
return overwrite;
}
return dirty;
}
+ /**
+ * Unset the dirty flag.
+ */
+ public void setNotDirty() {
+ dirty = false;
+ }
+
/**
* Save contents to file.
*
"UTF-8");
for (Line line: lines) {
- output.write(line.getRawString());
+ if (saveWithTabs) {
+ output.write(convertSpacesToTabs(line.getRawString()));
+ } else {
+ output.write(line.getRawString());
+ }
output.write("\n");
}
// If at the beginning of a word already, push past it.
if ((getChar() != -1)
&& (getRawLine().length() > 0)
- && !Character.isSpace((char) getChar())
+ && !Character.isWhitespace((char) getChar())
) {
left();
}
// int line = lineNumber;
while ((getChar() == -1)
|| (getRawLine().length() == 0)
- || Character.isSpace((char) getChar())
+ || Character.isWhitespace((char) getChar())
) {
if (left() == false) {
return;
assert (getChar() != -1);
- if (!Character.isSpace((char) getChar())
+ if (!Character.isWhitespace((char) getChar())
&& (getRawLine().length() > 0)
) {
// Advance until at the beginning of the document or a whitespace
// is encountered.
- while (!Character.isSpace((char) getChar())) {
+ while (!Character.isWhitespace((char) getChar())) {
int line = lineNumber;
if (left() == false) {
// End of document, bail out.
}
if (lineNumber != line) {
// We wrapped a line. Here that counts as whitespace.
- if (!Character.isSpace((char) getChar())) {
+ if (!Character.isWhitespace((char) getChar())) {
// We found a character immediately after the line.
// Done!
return;
}
assert (getChar() != -1);
- if (!Character.isSpace((char) getChar())
+ if (!Character.isWhitespace((char) getChar())
&& (getRawLine().length() > 0)
) {
// Advance until at the end of the document or a whitespace is
// encountered.
- while (!Character.isSpace((char) getChar())) {
+ while (!Character.isWhitespace((char) getChar())) {
line = lineNumber;
if (right() == false) {
// End of document, bail out.
}
if (lineNumber != line) {
// We wrapped a line. Here that counts as whitespace.
- if (!Character.isSpace((char) getChar())
+ if (!Character.isWhitespace((char) getChar())
&& (getRawLine().length() > 0)
) {
// We found a character immediately after the line.
}
if (lineNumber != line) {
// We wrapped a line. Here that counts as whitespace.
- if (!Character.isSpace((char) getChar())) {
+ if (!Character.isWhitespace((char) getChar())) {
// We found a character immediately after the line.
// Done!
return;
}
assert (getChar() != -1);
- if (Character.isSpace((char) getChar())) {
+ if (Character.isWhitespace((char) getChar())) {
// Advance until at the end of the document or a non-whitespace
// is encountered.
- while (Character.isSpace((char) getChar())) {
+ while (Character.isWhitespace((char) getChar())) {
if (right() == false) {
// End of document, bail out.
return;
dirty = true;
int cursor = lines.get(lineNumber).getCursor();
if (cursor > 0) {
- lines.get(lineNumber).backspace();
+ lines.get(lineNumber).backspace(tabSize, backspaceUnindents);
} else if (lineNumber > 0) {
// Join two lines
lineNumber--;
}
}
+ /**
+ * Get the tab stop size.
+ *
+ * @return the tab stop size
+ */
+ public int getTabSize() {
+ return tabSize;
+ }
+
+ /**
+ * Set the tab stop size.
+ *
+ * @param tabSize the new tab stop size
+ */
+ public void setTabSize(final int tabSize) {
+ this.tabSize = tabSize;
+ }
+
+ /**
+ * Set the backspace unindent option.
+ *
+ * @param backspaceUnindents If true, backspace at an indent level goes
+ * back a full indent level. If false, backspace always goes back one
+ * column.
+ */
+ public void setBackspaceUnindents(final boolean backspaceUnindents) {
+ this.backspaceUnindents = backspaceUnindents;
+ }
+
+ /**
+ * Set the save with tabs option.
+ *
+ * @param saveWithTabs If true, save files with tab characters. If
+ * false, convert tabs to spaces when saving files.
+ */
+ public void setSaveWithTabs(final boolean saveWithTabs) {
+ this.saveWithTabs = saveWithTabs;
+ }
+
+ /**
+ * Handle the tab character.
+ */
+ public void tab() {
+ if (overwrite) {
+ del();
+ }
+ lines.get(lineNumber).tab(tabSize);
+ }
+
+ /**
+ * Handle the backtab (shift-tab) character.
+ */
+ public void backTab() {
+ lines.get(lineNumber).backTab(tabSize);
+ }
+
/**
* Get a (shallow) copy of the list of lines.
*
return lines.get(lineNumber).getDisplayLength();
}
+ /**
+ * Get the entire contents of the document as one string.
+ *
+ * @return the document contents
+ */
+ public String getText() {
+ StringBuilder sb = new StringBuilder();
+ for (Line line: getLines()) {
+ sb.append(line.getRawString());
+ sb.append("\n");
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Trim trailing whitespace from lines and trailing empty
+ * lines from the document.
+ */
+ public void cleanWhitespace() {
+ for (Line line: getLines()) {
+ line.trimRight();
+ }
+ if (lines.size() == 0) {
+ return;
+ }
+ while (lines.get(lines.size() - 1).length() == 0) {
+ lines.remove(lines.size() - 1);
+ }
+ if (lineNumber > lines.size() - 1) {
+ lineNumber = lines.size() - 1;
+ }
+ }
+
+ /**
+ * Set keyword highlighting.
+ *
+ * @param enabled if true, enable keyword highlighting
+ */
+ public void setHighlighting(final boolean enabled) {
+ highlighter.setEnabled(enabled);
+ for (Line line: getLines()) {
+ line.scanLine();
+ }
+ }
+
+ /**
+ * Convert a string with leading spaces to a mix of tabs and spaces.
+ *
+ * @param string the string to convert
+ */
+ private String convertSpacesToTabs(final String string) {
+ if (string.length() == 0) {
+ return string;
+ }
+
+ int start = 0;
+ while (string.charAt(start) == ' ') {
+ start++;
+ }
+ int tabCount = start / 8;
+ if (tabCount == 0) {
+ return string;
+ }
+
+ StringBuilder sb = new StringBuilder(string.length());
+
+ for (int i = 0; i < tabCount; i++) {
+ sb.append('\t');
+ }
+ sb.append(string.substring(tabCount * 8));
+ return sb.toString();
+ }
+
}
* Public constructor sets the theme to the default.
*/
public Highlighter() {
- colors = new TreeMap<String, CellAttributes>();
+ // NOP
}
// ------------------------------------------------------------------------
// Highlighter ------------------------------------------------------------
// ------------------------------------------------------------------------
+ /**
+ * Set keyword highlighting.
+ *
+ * @param enabled if true, enable keyword highlighting
+ */
+ public void setEnabled(final boolean enabled) {
+ if (enabled) {
+ setJavaColors();
+ } else {
+ colors = null;
+ }
+ }
+
+ /**
+ * Set my field values to that's field.
+ *
+ * @param rhs an instance of Highlighter
+ */
+ public void setTo(final Highlighter rhs) {
+ colors = new TreeMap<String, CellAttributes>();
+ colors.putAll(rhs.colors);
+ }
+
/**
* See if this is a character that should split a word.
*
* @return color associated with name, e.g. bold yellow on blue
*/
public CellAttributes getColor(final String name) {
- CellAttributes attr = (CellAttributes) colors.get(name);
+ if (colors == null) {
+ return null;
+ }
+ CellAttributes attr = colors.get(name);
return attr;
}
* Sets to defaults that resemble the Borland IDE colors.
*/
public void setJavaColors() {
+ colors = new TreeMap<String, CellAttributes>();
+
CellAttributes color;
- String [] keywords = {
+ String [] types = {
"boolean", "byte", "short", "int", "long", "char", "float",
- "double", "void", "new",
- "static", "final", "volatile", "synchronized", "abstract",
- "public", "private", "protected",
- "class", "interface", "extends", "implements",
+ "double", "void",
+ };
+ color = new CellAttributes();
+ color.setForeColor(Color.GREEN);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ for (String str: types) {
+ colors.put(str, color);
+ }
+
+ String [] modifiers = {
+ "abstract", "final", "native", "private", "protected", "public",
+ "static", "strictfp", "synchronized", "transient", "volatile",
+ };
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ for (String str: modifiers) {
+ colors.put(str, color);
+ }
+
+ String [] keywords = {
+ "new", "class", "interface", "extends", "implements",
"if", "else", "do", "while", "for", "break", "continue",
"switch", "case", "default",
};
color = new CellAttributes();
- color.setForeColor(Color.WHITE);
+ color.setForeColor(Color.YELLOW);
color.setBackColor(Color.BLUE);
color.setBold(true);
for (String str: keywords) {
import java.util.List;
import jexer.bits.CellAttributes;
+import jexer.bits.GraphicsChars;
import jexer.bits.StringUtils;
/**
this.defaultColor = defaultColor;
this.highlighter = highlighter;
- this.rawText = new StringBuilder(str);
+
+ this.rawText = new StringBuilder();
+ int col = 0;
+ for (int i = 0; i < str.length(); i++) {
+ char ch = str.charAt(i);
+ if (ch == '\t') {
+ // Expand tabs
+ int j = col % 8;
+ do {
+ rawText.append(' ');
+ j++;
+ col++;
+ } while ((j % 8) != 0);
+ continue;
+ }
+ if ((ch <= 0x20) || (ch == 0x7F)) {
+ // Replace all other C0 bytes with CP437 glyphs.
+ rawText.append(GraphicsChars.CP437[(int) ch]);
+ col++;
+ continue;
+ }
+
+ rawText.append(ch);
+ col++;
+ }
scanLine();
}
this(str, defaultColor, null);
}
+ /**
+ * Private constructor used by dup().
+ */
+ private Line() {
+ // NOP
+ }
+
// ------------------------------------------------------------------------
// Line -------------------------------------------------------------------
// ------------------------------------------------------------------------
+ /**
+ * Create a duplicate instance.
+ *
+ * @return duplicate intance
+ */
+ public Line dup() {
+ Line other = new Line();
+ other.defaultColor = defaultColor;
+ other.highlighter = highlighter;
+ other.position = position;
+ other.screenPosition = screenPosition;
+ other.rawText = new StringBuilder(rawText);
+ other.scanLine();
+ return other;
+ }
+
/**
* Get a (shallow) copy of the words in this line.
*
}
/**
- * Scan rawText and make words out of it.
+ * Get the raw length of this line.
+ *
+ * @return the length of this line in characters, which may be different
+ * from the number of cells needed to display it
+ */
+ public int length() {
+ return rawText.length();
+ }
+
+ /**
+ * Scan rawText and make words out of it. Note package private access.
*/
- private void scanLine() {
+ void scanLine() {
words.clear();
Word word = new Word(this.defaultColor, this.highlighter);
words.add(word);
if (getDisplayLength() == 0) {
return false;
}
- if (position == getDisplayLength() - 1) {
+ if (screenPosition == getDisplayLength() - 1) {
return false;
}
if (position < rawText.length()) {
* @return true if the cursor position changed
*/
public boolean end() {
- if (position != getDisplayLength() - 1) {
+ if (screenPosition != getDisplayLength() - 1) {
position = rawText.length();
screenPosition = StringUtils.width(rawText.toString());
return true;
public void del() {
assert (words.size() > 0);
- if (position < getDisplayLength()) {
+ if (screenPosition < getDisplayLength()) {
int n = Character.charCount(rawText.codePointAt(position));
for (int i = 0; i < n; i++) {
rawText.deleteCharAt(position);
/**
* Delete the character immediately preceeding the cursor.
+ *
+ * @param tabSize the tab stop size
+ * @param backspaceUnindents If true, backspace at an indent level goes
+ * back a full indent level. If false, backspace always goes back one
+ * column.
*/
- public void backspace() {
+ public void backspace(final int tabSize, final boolean backspaceUnindents) {
+ if ((backspaceUnindents == true)
+ && (tabSize > 0)
+ && (screenPosition > 0)
+ && (rawText.charAt(position - 1) == ' ')
+ && ((screenPosition % tabSize) == 0)
+ ) {
+ boolean doBackTab = true;
+ for (int i = 0; i < position; i++) {
+ if (rawText.charAt(i) != ' ') {
+ doBackTab = false;
+ break;
+ }
+ }
+ if (doBackTab) {
+ backTab(tabSize);
+ return;
+ }
+ }
+
if (left()) {
del();
}
* @param ch the character to insert
*/
public void addChar(final int ch) {
- if (position < getDisplayLength() - 1) {
+ if (screenPosition < getDisplayLength() - 1) {
rawText.insert(position, Character.toChars(ch));
} else {
rawText.append(Character.toChars(ch));
* @param ch the character to replace
*/
public void replaceChar(final int ch) {
- if (position < getDisplayLength() - 1) {
+ if (screenPosition < getDisplayLength() - 1) {
// Replace character
String oldText = rawText.toString();
rawText = new StringBuilder(oldText.substring(0, position));
* @param screenPosition the position on screen
* @return the equivalent position in text
*/
- protected int screenToTextPosition(final int screenPosition) {
+ private int screenToTextPosition(final int screenPosition) {
if (screenPosition == 0) {
return 0;
}
" exceeds available text length " + rawText.length());
}
+ /**
+ * Trim trailing whitespace from line, repositioning cursor if needed.
+ */
+ public void trimRight() {
+ if (rawText.length() == 0) {
+ return;
+ }
+ if (!Character.isWhitespace(rawText.charAt(rawText.length() - 1))) {
+ return;
+ }
+ while ((rawText.length() > 0)
+ && Character.isWhitespace(rawText.charAt(rawText.length() - 1))
+ ) {
+ rawText.deleteCharAt(rawText.length() - 1);
+ }
+ if (position >= rawText.length()) {
+ end();
+ }
+ scanLine();
+ }
+
+ /**
+ * Handle the tab character.
+ *
+ * @param tabSize the tab stop size
+ */
+ public void tab(final int tabSize) {
+ if (tabSize > 0) {
+ do {
+ addChar(' ');
+ } while ((screenPosition % tabSize) != 0);
+ }
+ }
+
+ /**
+ * Handle the backtab (shift-tab) character.
+ *
+ * @param tabSize the tab stop size
+ */
+ public void backTab(final int tabSize) {
+ if ((tabSize > 0) && (screenPosition > 0)
+ && (rawText.charAt(position - 1) == ' ')
+ ) {
+ do {
+ backspace(tabSize, false);
+ } while (((screenPosition % tabSize) != 0)
+ && (screenPosition > 0)
+ && (rawText.charAt(position - 1) == ' '));
+ }
+ }
+
}
* @return the number of cells needed to display this word
*/
public int getDisplayLength() {
- // For now, just use the text length. In the future, this will be a
- // grapheme count.
-
- // TODO: figure out how to handle the tab character. Do we have a
- // global tab stops list and current word position?
return StringUtils.width(text.toString());
}
chars[chars.length - 1] = new Cell(newCell);
}
+ /**
+ * Determine if line contains image data.
+ *
+ * @return true if the line has image data
+ */
+ public boolean isImage() {
+ for (int i = 0; i < chars.length; i++) {
+ if (chars[i].isImage()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Clear image data from line.
+ */
+ public void clearImages() {
+ for (int i = 0; i < chars.length; i++) {
+ if (chars[i].isImage()) {
+ chars[i].reset();
+ }
+ }
+ }
+
}
*/
package jexer.tterminal;
-import java.awt.Graphics2D;
+import java.awt.Graphics;
import java.awt.image.BufferedImage;
+import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
import java.io.CharArrayWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import javax.imageio.ImageIO;
import jexer.TKeypress;
import jexer.backend.GlyphMaker;
/**
* The type of emulator to be.
*/
- private DeviceType type = DeviceType.VT102;
+ private final DeviceType type;
/**
* The scrollback buffer characters + attributes.
/**
* The maximum number of lines in the scrollback buffer.
*/
- private int maxScrollback = 10000;
+ private int scrollbackMax = 10000;
/**
* The terminal's input. For type == XTERM, this is an InputStreamReader
* Physical display width. We start at 80x24, but the user can resize us
* bigger/smaller.
*/
- private int width;
+ private int width = 80;
/**
* Physical display height. We start at 80x24, but the user can resize
* us bigger/smaller.
*/
- private int height;
+ private int height = 24;
/**
* Top margin of the scrolling region.
*/
- private int scrollRegionTop;
+ private int scrollRegionTop = 0;
/**
* Bottom margin of the scrolling region.
*/
- private int scrollRegionBottom;
+ private int scrollRegionBottom = height - 1;
/**
* Right margin column number. This can be selected by the remote side
* to be 80/132 (rightMargin values 79/131), or it can be (width - 1).
*/
- private int rightMargin;
+ private int rightMargin = 79;
/**
* Last character printed.
* 132), but the line does NOT wrap until another character is written to
* column 1 of the next line, after which the cursor moves to column 2.
*/
- private boolean wrapLineFlag;
+ private boolean wrapLineFlag = false;
/**
* VT220 single shift flag.
/**
* Non-csi collect buffer.
*/
- private StringBuilder collectBuffer;
+ private StringBuilder collectBuffer = new StringBuilder(128);
/**
* When true, use the G1 character set.
/**
* Sixel collection buffer.
*/
- private StringBuilder sixelParseBuffer;
+ private StringBuilder sixelParseBuffer = new StringBuilder(2048);
/**
* Sixel shared palette.
*/
private ArrayList<TInputEvent> userQueue = new ArrayList<TInputEvent>();
+ /**
+ * Number of bytes/characters passed to consume().
+ */
+ private long readCount = 0;
+
/**
* DECSC/DECRC save/restore a subset of the total state. This class
* encapsulates those specific flags/modes.
this.inputStream = new TimeoutInputStream(inputStream, 2000);
}
if (type == DeviceType.XTERM) {
- this.input = new InputStreamReader(this.inputStream, "UTF-8");
+ this.input = new InputStreamReader(new BufferedInputStream(
+ this.inputStream, 1024 * 128), "UTF-8");
this.output = new OutputStreamWriter(new
BufferedOutputStream(outputStream), "UTF-8");
this.outputStream = null;
for (int i = 0; i < height; i++) {
display.add(new DisplayLine(currentState.attr));
}
+ assert (currentState.cursorY < height);
+ assert (currentState.cursorX < width);
// Spin up the input reader
readerThread = new Thread(this);
int ch = Character.codePointAt(readBufferUTF8,
i);
i += Character.charCount(ch);
- consume(ch);
+
+ // Special case for VT10x: 7-bit characters
+ // only.
+ if ((type == DeviceType.VT100)
+ || (type == DeviceType.VT102)
+ ) {
+ consume(ch & 0x7F);
+ } else {
+ consume(ch);
+ }
}
} else {
for (int i = 0; i < rc; i++) {
- consume(readBuffer[i]);
+ // Special case for VT10x: 7-bit characters
+ // only.
+ if ((type == DeviceType.VT100)
+ || (type == DeviceType.VT102)
+ ) {
+ consume(readBuffer[i] & 0x7F);
+ } else {
+ consume(readBuffer[i]);
+ }
}
}
}
// ECMA48 -----------------------------------------------------------------
// ------------------------------------------------------------------------
+ /**
+ * Wait for a period of time to get output from the launched process.
+ *
+ * @param millis millis to wait for, or 0 to wait forever
+ * @return true if the launched process has emitted something
+ */
+ public boolean waitForOutput(final int millis) {
+ if (millis < 0) {
+ throw new IllegalArgumentException("timeout must be >= 0");
+ }
+ int waitedMillis = millis;
+ final int pollTimeout = 5;
+ while (true) {
+ if (readCount != 0) {
+ return true;
+ }
+ if ((millis > 0) && (waitedMillis < 0)){
+ return false;
+ }
+ try {
+ Thread.sleep(pollTimeout);
+ } catch (InterruptedException e) {
+ // SQUASH
+ }
+ waitedMillis -= pollTimeout;
+ }
+ }
+
/**
* Process keyboard and mouse events from the user.
*
case VT220:
case XTERM:
- // "I am a VT220" - 7 bit version
+ // "I am a VT220" - 7 bit version, with sixel and Jexer image
+ // support.
if (!s8c1t) {
- return "\033[?62;1;6;9;4;22c";
- // return "\033[?62;1;6;9;4;22;444c";
+ return "\033[?62;1;6;9;4;22;444c";
}
- // "I am a VT220" - 8 bit version
- return "\u009b?62;1;6;9;4;22c";
- // return "\u009b?62;1;6;9;4;22;444c";
+ // "I am a VT220" - 8 bit version, with sixel and Jexer image
+ // support.
+ return "\u009b?62;1;6;9;4;22;444c";
default:
throw new IllegalArgumentException("Invalid device type: " + type);
}
// the input streams.
if (stopReaderThread == false) {
stopReaderThread = true;
- try {
- readerThread.join(1000);
- } catch (InterruptedException e) {
- // SQUASH
- }
}
// Now close the output stream.
int delta = height - this.height;
this.height = height;
scrollRegionBottom += delta;
- if (scrollRegionBottom < 0) {
- scrollRegionBottom = height;
+ if ((scrollRegionBottom < 0) || (scrollRegionTop > height - 1)) {
+ scrollRegionBottom = height - 1;
}
if (scrollRegionTop >= scrollRegionBottom) {
scrollRegionTop = 0;
display.add(line);
}
while (display.size() > height) {
- scrollback.add(display.remove(0));
+ appendScrollbackLine(display.remove(0));
}
}
+ /**
+ * Get the maximum number of lines in the scrollback buffer.
+ *
+ * @return the maximum number of lines in the scrollback buffer
+ */
+ public int getScrollbackMax() {
+ return scrollbackMax;
+ }
+
+ /**
+ * Set the maximum number of lines for the scrollback buffer.
+ *
+ * @param scrollbackMax the maximum number of lines for the scrollback
+ * buffer
+ */
+ public final void setScrollbackMax(final int scrollbackMax) {
+ this.scrollbackMax = scrollbackMax;
+ }
+
/**
* Get visible cursor flag.
*
*/
private void toGround() {
csiParams.clear();
- collectBuffer = new StringBuilder(8);
+ collectBuffer.setLength(0);
scanState = ScanState.GROUND;
}
colors88.add(0);
}
- // Set default system colors.
+ // Set default system colors. These match DOS colors.
colors88.set(0, 0x00000000);
colors88.set(1, 0x00a80000);
colors88.set(2, 0x0000a800);
colors88.set(13, 0x00fc54fc);
colors88.set(14, 0x0054fcfc);
colors88.set(15, 0x00fcfcfc);
+
+ // These match xterm's default colors from 256colres.h.
+ colors88.set(16, 0x000000);
+ colors88.set(17, 0x00005f);
+ colors88.set(18, 0x000087);
+ colors88.set(19, 0x0000af);
+ colors88.set(20, 0x0000d7);
+ colors88.set(21, 0x0000ff);
+ colors88.set(22, 0x005f00);
+ colors88.set(23, 0x005f5f);
+ colors88.set(24, 0x005f87);
+ colors88.set(25, 0x005faf);
+ colors88.set(26, 0x005fd7);
+ colors88.set(27, 0x005fff);
+ colors88.set(28, 0x008700);
+ colors88.set(29, 0x00875f);
+ colors88.set(30, 0x008787);
+ colors88.set(31, 0x0087af);
+ colors88.set(32, 0x0087d7);
+ colors88.set(33, 0x0087ff);
+ colors88.set(34, 0x00af00);
+ colors88.set(35, 0x00af5f);
+ colors88.set(36, 0x00af87);
+ colors88.set(37, 0x00afaf);
+ colors88.set(38, 0x00afd7);
+ colors88.set(39, 0x00afff);
+ colors88.set(40, 0x00d700);
+ colors88.set(41, 0x00d75f);
+ colors88.set(42, 0x00d787);
+ colors88.set(43, 0x00d7af);
+ colors88.set(44, 0x00d7d7);
+ colors88.set(45, 0x00d7ff);
+ colors88.set(46, 0x00ff00);
+ colors88.set(47, 0x00ff5f);
+ colors88.set(48, 0x00ff87);
+ colors88.set(49, 0x00ffaf);
+ colors88.set(50, 0x00ffd7);
+ colors88.set(51, 0x00ffff);
+ colors88.set(52, 0x5f0000);
+ colors88.set(53, 0x5f005f);
+ colors88.set(54, 0x5f0087);
+ colors88.set(55, 0x5f00af);
+ colors88.set(56, 0x5f00d7);
+ colors88.set(57, 0x5f00ff);
+ colors88.set(58, 0x5f5f00);
+ colors88.set(59, 0x5f5f5f);
+ colors88.set(60, 0x5f5f87);
+ colors88.set(61, 0x5f5faf);
+ colors88.set(62, 0x5f5fd7);
+ colors88.set(63, 0x5f5fff);
+ colors88.set(64, 0x5f8700);
+ colors88.set(65, 0x5f875f);
+ colors88.set(66, 0x5f8787);
+ colors88.set(67, 0x5f87af);
+ colors88.set(68, 0x5f87d7);
+ colors88.set(69, 0x5f87ff);
+ colors88.set(70, 0x5faf00);
+ colors88.set(71, 0x5faf5f);
+ colors88.set(72, 0x5faf87);
+ colors88.set(73, 0x5fafaf);
+ colors88.set(74, 0x5fafd7);
+ colors88.set(75, 0x5fafff);
+ colors88.set(76, 0x5fd700);
+ colors88.set(77, 0x5fd75f);
+ colors88.set(78, 0x5fd787);
+ colors88.set(79, 0x5fd7af);
+ colors88.set(80, 0x5fd7d7);
+ colors88.set(81, 0x5fd7ff);
+ colors88.set(82, 0x5fff00);
+ colors88.set(83, 0x5fff5f);
+ colors88.set(84, 0x5fff87);
+ colors88.set(85, 0x5fffaf);
+ colors88.set(86, 0x5fffd7);
+ colors88.set(87, 0x5fffff);
+ colors88.set(88, 0x870000);
+ colors88.set(89, 0x87005f);
+ colors88.set(90, 0x870087);
+ colors88.set(91, 0x8700af);
+ colors88.set(92, 0x8700d7);
+ colors88.set(93, 0x8700ff);
+ colors88.set(94, 0x875f00);
+ colors88.set(95, 0x875f5f);
+ colors88.set(96, 0x875f87);
+ colors88.set(97, 0x875faf);
+ colors88.set(98, 0x875fd7);
+ colors88.set(99, 0x875fff);
+ colors88.set(100, 0x878700);
+ colors88.set(101, 0x87875f);
+ colors88.set(102, 0x878787);
+ colors88.set(103, 0x8787af);
+ colors88.set(104, 0x8787d7);
+ colors88.set(105, 0x8787ff);
+ colors88.set(106, 0x87af00);
+ colors88.set(107, 0x87af5f);
+ colors88.set(108, 0x87af87);
+ colors88.set(109, 0x87afaf);
+ colors88.set(110, 0x87afd7);
+ colors88.set(111, 0x87afff);
+ colors88.set(112, 0x87d700);
+ colors88.set(113, 0x87d75f);
+ colors88.set(114, 0x87d787);
+ colors88.set(115, 0x87d7af);
+ colors88.set(116, 0x87d7d7);
+ colors88.set(117, 0x87d7ff);
+ colors88.set(118, 0x87ff00);
+ colors88.set(119, 0x87ff5f);
+ colors88.set(120, 0x87ff87);
+ colors88.set(121, 0x87ffaf);
+ colors88.set(122, 0x87ffd7);
+ colors88.set(123, 0x87ffff);
+ colors88.set(124, 0xaf0000);
+ colors88.set(125, 0xaf005f);
+ colors88.set(126, 0xaf0087);
+ colors88.set(127, 0xaf00af);
+ colors88.set(128, 0xaf00d7);
+ colors88.set(129, 0xaf00ff);
+ colors88.set(130, 0xaf5f00);
+ colors88.set(131, 0xaf5f5f);
+ colors88.set(132, 0xaf5f87);
+ colors88.set(133, 0xaf5faf);
+ colors88.set(134, 0xaf5fd7);
+ colors88.set(135, 0xaf5fff);
+ colors88.set(136, 0xaf8700);
+ colors88.set(137, 0xaf875f);
+ colors88.set(138, 0xaf8787);
+ colors88.set(139, 0xaf87af);
+ colors88.set(140, 0xaf87d7);
+ colors88.set(141, 0xaf87ff);
+ colors88.set(142, 0xafaf00);
+ colors88.set(143, 0xafaf5f);
+ colors88.set(144, 0xafaf87);
+ colors88.set(145, 0xafafaf);
+ colors88.set(146, 0xafafd7);
+ colors88.set(147, 0xafafff);
+ colors88.set(148, 0xafd700);
+ colors88.set(149, 0xafd75f);
+ colors88.set(150, 0xafd787);
+ colors88.set(151, 0xafd7af);
+ colors88.set(152, 0xafd7d7);
+ colors88.set(153, 0xafd7ff);
+ colors88.set(154, 0xafff00);
+ colors88.set(155, 0xafff5f);
+ colors88.set(156, 0xafff87);
+ colors88.set(157, 0xafffaf);
+ colors88.set(158, 0xafffd7);
+ colors88.set(159, 0xafffff);
+ colors88.set(160, 0xd70000);
+ colors88.set(161, 0xd7005f);
+ colors88.set(162, 0xd70087);
+ colors88.set(163, 0xd700af);
+ colors88.set(164, 0xd700d7);
+ colors88.set(165, 0xd700ff);
+ colors88.set(166, 0xd75f00);
+ colors88.set(167, 0xd75f5f);
+ colors88.set(168, 0xd75f87);
+ colors88.set(169, 0xd75faf);
+ colors88.set(170, 0xd75fd7);
+ colors88.set(171, 0xd75fff);
+ colors88.set(172, 0xd78700);
+ colors88.set(173, 0xd7875f);
+ colors88.set(174, 0xd78787);
+ colors88.set(175, 0xd787af);
+ colors88.set(176, 0xd787d7);
+ colors88.set(177, 0xd787ff);
+ colors88.set(178, 0xd7af00);
+ colors88.set(179, 0xd7af5f);
+ colors88.set(180, 0xd7af87);
+ colors88.set(181, 0xd7afaf);
+ colors88.set(182, 0xd7afd7);
+ colors88.set(183, 0xd7afff);
+ colors88.set(184, 0xd7d700);
+ colors88.set(185, 0xd7d75f);
+ colors88.set(186, 0xd7d787);
+ colors88.set(187, 0xd7d7af);
+ colors88.set(188, 0xd7d7d7);
+ colors88.set(189, 0xd7d7ff);
+ colors88.set(190, 0xd7ff00);
+ colors88.set(191, 0xd7ff5f);
+ colors88.set(192, 0xd7ff87);
+ colors88.set(193, 0xd7ffaf);
+ colors88.set(194, 0xd7ffd7);
+ colors88.set(195, 0xd7ffff);
+ colors88.set(196, 0xff0000);
+ colors88.set(197, 0xff005f);
+ colors88.set(198, 0xff0087);
+ colors88.set(199, 0xff00af);
+ colors88.set(200, 0xff00d7);
+ colors88.set(201, 0xff00ff);
+ colors88.set(202, 0xff5f00);
+ colors88.set(203, 0xff5f5f);
+ colors88.set(204, 0xff5f87);
+ colors88.set(205, 0xff5faf);
+ colors88.set(206, 0xff5fd7);
+ colors88.set(207, 0xff5fff);
+ colors88.set(208, 0xff8700);
+ colors88.set(209, 0xff875f);
+ colors88.set(210, 0xff8787);
+ colors88.set(211, 0xff87af);
+ colors88.set(212, 0xff87d7);
+ colors88.set(213, 0xff87ff);
+ colors88.set(214, 0xffaf00);
+ colors88.set(215, 0xffaf5f);
+ colors88.set(216, 0xffaf87);
+ colors88.set(217, 0xffafaf);
+ colors88.set(218, 0xffafd7);
+ colors88.set(219, 0xffafff);
+ colors88.set(220, 0xffd700);
+ colors88.set(221, 0xffd75f);
+ colors88.set(222, 0xffd787);
+ colors88.set(223, 0xffd7af);
+ colors88.set(224, 0xffd7d7);
+ colors88.set(225, 0xffd7ff);
+ colors88.set(226, 0xffff00);
+ colors88.set(227, 0xffff5f);
+ colors88.set(228, 0xffff87);
+ colors88.set(229, 0xffffaf);
+ colors88.set(230, 0xffffd7);
+ colors88.set(231, 0xffffff);
+ colors88.set(232, 0x080808);
+ colors88.set(233, 0x121212);
+ colors88.set(234, 0x1c1c1c);
+ colors88.set(235, 0x262626);
+ colors88.set(236, 0x303030);
+ colors88.set(237, 0x3a3a3a);
+ colors88.set(238, 0x444444);
+ colors88.set(239, 0x4e4e4e);
+ colors88.set(240, 0x585858);
+ colors88.set(241, 0x626262);
+ colors88.set(242, 0x6c6c6c);
+ colors88.set(243, 0x767676);
+ colors88.set(244, 0x808080);
+ colors88.set(245, 0x8a8a8a);
+ colors88.set(246, 0x949494);
+ colors88.set(247, 0x9e9e9e);
+ colors88.set(248, 0xa8a8a8);
+ colors88.set(249, 0xb2b2b2);
+ colors88.set(250, 0xbcbcbc);
+ colors88.set(251, 0xc6c6c6);
+ colors88.set(252, 0xd0d0d0);
+ colors88.set(253, 0xdadada);
+ colors88.set(254, 0xe4e4e4);
+ colors88.set(255, 0xeeeeee);
+
}
/**
currentState = new SaveableState();
savedState = new SaveableState();
scanState = ScanState.GROUND;
- width = 80;
- height = 24;
+ if (displayListener != null) {
+ width = displayListener.getDisplayWidth();
+ height = displayListener.getDisplayHeight();
+ } else {
+ width = 80;
+ height = 24;
+ }
scrollRegionTop = 0;
scrollRegionBottom = height - 1;
rightMargin = width - 1;
arrowKeyMode = ArrowKeyMode.ANSI;
keypadMode = KeypadMode.Numeric;
wrapLineFlag = false;
- if (displayListener != null) {
- width = displayListener.getDisplayWidth();
- height = displayListener.getDisplayHeight();
- rightMargin = width - 1;
- }
// Flags
shiftOut = false;
toGround();
}
+ /**
+ * Append a to the scrollback buffer, clearing image data for lines more
+ * than three screenfuls in.
+ */
+ private void appendScrollbackLine(DisplayLine line) {
+ scrollback.add(line);
+ if (scrollback.size() > height * 3) {
+ scrollback.get(scrollback.size() - (height * 3)).clearImages();
+ }
+ }
+
/**
* Append a new line to the bottom of the display, adding lines off the
* top to the scrollback buffer.
*/
private void newDisplayLine() {
// Scroll the top line off into the scrollback buffer
- scrollback.add(display.get(0));
- if (scrollback.size() > maxScrollback) {
+ appendScrollbackLine(display.get(0));
+ while (scrollback.size() > scrollbackMax) {
scrollback.remove(0);
scrollback.trimToSize();
}
* Handle a linefeed.
*/
private void linefeed() {
-
if (currentState.cursorY < scrollRegionBottom) {
// Increment screen y
currentState.cursorY++;
if (mouseEncoding == MouseEncoding.SGR) {
sb.append((char) 0x1B);
sb.append("[<");
+ int buttons = 0;
if (mouse.isMouse1()) {
if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
- sb.append("32;");
+ buttons = 32;
} else {
- sb.append("0;");
+ buttons = 0;
}
} else if (mouse.isMouse2()) {
if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
- sb.append("33;");
+ buttons = 33;
} else {
- sb.append("1;");
+ buttons = 1;
}
} else if (mouse.isMouse3()) {
if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
- sb.append("34;");
+ buttons = 34;
} else {
- sb.append("2;");
+ buttons = 2;
}
} else if (mouse.isMouseWheelUp()) {
- sb.append("64;");
+ buttons = 64;
} else if (mouse.isMouseWheelDown()) {
- sb.append("65;");
+ buttons = 65;
} else {
// This is motion with no buttons down.
- sb.append("35;");
+ buttons = 35;
+ }
+ if (mouse.isAlt()) {
+ buttons |= 0x08;
+ }
+ if (mouse.isCtrl()) {
+ buttons |= 0x10;
+ }
+ if (mouse.isShift()) {
+ buttons |= 0x04;
}
- sb.append(String.format("%d;%d", mouse.getX() + 1,
+ sb.append(String.format("%d;%d;%d", buttons, mouse.getX() + 1,
mouse.getY() + 1));
if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
sb.append((char) 0x1B);
sb.append('[');
sb.append('M');
+ int buttons = 0;
if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
- sb.append((char) (0x03 + 32));
+ buttons = 0x03 + 32;
} else if (mouse.isMouse1()) {
if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
- sb.append((char) (0x00 + 32 + 32));
+ buttons = 0x00 + 32 + 32;
} else {
- sb.append((char) (0x00 + 32));
+ buttons = 0x00 + 32;
}
} else if (mouse.isMouse2()) {
if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
- sb.append((char) (0x01 + 32 + 32));
+ buttons = 0x01 + 32 + 32;
} else {
- sb.append((char) (0x01 + 32));
+ buttons = 0x01 + 32;
}
} else if (mouse.isMouse3()) {
if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
- sb.append((char) (0x02 + 32 + 32));
+ buttons = 0x02 + 32 + 32;
} else {
- sb.append((char) (0x02 + 32));
+ buttons = 0x02 + 32;
}
} else if (mouse.isMouseWheelUp()) {
- sb.append((char) (0x04 + 64));
+ buttons = 0x04 + 64;
} else if (mouse.isMouseWheelDown()) {
- sb.append((char) (0x05 + 64));
+ buttons = 0x05 + 64;
} else {
// This is motion with no buttons down.
- sb.append((char) (0x03 + 32));
+ buttons = 0x03 + 32;
+ }
+ if (mouse.isAlt()) {
+ buttons |= 0x08;
+ }
+ if (mouse.isCtrl()) {
+ buttons |= 0x10;
+ }
+ if (mouse.isShift()) {
+ buttons |= 0x04;
}
+ sb.append((char) (buttons & 0xFF));
sb.append((char) (mouse.getX() + 33));
sb.append((char) (mouse.getY() + 33));
}
if (decPrivateModeFlag == true) {
if (value == true) {
// Enable sixel scrolling (default).
- // TODO
+ // Not supported
} else {
// Disable sixel scrolling.
- // TODO
+ // Not supported
}
}
}
* RGB color mode.
*/
rgbColor = true;
- break;
+ continue;
case 5:
/*
* Indexed color mode.
*/
idx88Color = true;
- break;
+ continue;
default:
/*
case 8:
// Invisible
- // TODO
+ // Not supported
break;
case 90:
// DECSTBM
int top = getCsiParam(0, 1, 1, height) - 1;
int bottom = getCsiParam(1, height, 1, height) - 1;
+ if (bottom > height - 1) {
+ bottom = height - 1;
+ }
if (top > bottom) {
top = bottom;
private void oscPut(final char xtermChar) {
// System.err.println("oscPut: " + xtermChar);
+ boolean oscEnd = false;
+
+ if (xtermChar == 0x07) {
+ oscEnd = true;
+ }
+ if ((xtermChar == '\\')
+ && (collectBuffer.charAt(collectBuffer.length() - 1) == '\033')
+ ) {
+ oscEnd = true;
+ }
+
// Collect first
collectBuffer.append(xtermChar);
// Xterm cases...
- if ((xtermChar == 0x07)
- || (collectBuffer.toString().endsWith("\033\\"))
- ) {
+ if (oscEnd) {
String args = null;
if (xtermChar == 0x07) {
args = collectBuffer.substring(0, collectBuffer.length() - 1);
}
}
- if (p[0].equals("444") && (p.length == 5)) {
- // Jexer image
- parseJexerImage(p[1], p[2], p[3], p[4]);
+ if (p[0].equals("444")) {
+ if (p[1].equals("0") && (p.length == 6)) {
+ // Jexer image - RGB
+ parseJexerImageRGB(p[2], p[3], p[4], p[5]);
+ } else if (p[1].equals("1") && (p.length == 4)) {
+ // Jexer image - PNG
+ parseJexerImageFile(1, p[2], p[3]);
+ } else if (p[1].equals("2") && (p.length == 4)) {
+ // Jexer image - JPG
+ parseJexerImageFile(2, p[2], p[3]);
+ }
}
-
}
// Go to SCAN_GROUND state
private void pmPut(final char pmChar) {
// System.err.println("pmPut: " + pmChar);
+ boolean pmEnd = false;
+
+ if ((pmChar == '\\')
+ && (collectBuffer.charAt(collectBuffer.length() - 1) == '\033')
+ ) {
+ pmEnd = true;
+ }
+
// Collect first
collectBuffer.append(pmChar);
// Xterm cases...
- if (collectBuffer.toString().endsWith("\033\\")) {
+ if (pmEnd) {
String arg = null;
arg = collectBuffer.substring(0, collectBuffer.length() - 2);
*
* @param ch character from the remote side
*/
- private void consume(int ch) {
+ private void consume(final int ch) {
+ readCount++;
// DEBUG
// System.err.printf("%c STATE = %s\n", ch, scanState);
- // Special case for VT10x: 7-bit characters only
- if ((type == DeviceType.VT100) || (type == DeviceType.VT102)) {
- ch = (ch & 0x7F);
- }
-
// Special "anywhere" states
// 18, 1A --> execute, then switch to SCAN_GROUND
// 0x71 goes to DCS_SIXEL
if (ch == 0x71) {
- sixelParseBuffer = new StringBuilder();
+ sixelParseBuffer.setLength(0);
scanState = ScanState.DCS_SIXEL;
} else if ((ch >= 0x40) && (ch <= 0x7E)) {
// 0x40-7E goes to DCS_PASSTHROUGH
// 0x71 goes to DCS_SIXEL
if (ch == 0x71) {
- sixelParseBuffer = new StringBuilder();
+ sixelParseBuffer.setLength(0);
scanState = ScanState.DCS_SIXEL;
} else if ((ch >= 0x40) && (ch <= 0x7E)) {
// 0x40-7E goes to DCS_PASSTHROUGH
// Sixel data was malformed in some way, bail out.
return;
}
-
- /*
- * Procedure:
- *
- * Break up the image into text cell sized pieces as a new array of
- * Cells.
- *
- * Note original column position x0.
- *
- * For each cell:
- *
- * 1. Advance (printCharacter(' ')) for horizontal increment, or
- * index (linefeed() + cursorPosition(y, x0)) for vertical
- * increment.
- *
- * 2. Set (x, y) cell image data.
- *
- * 3. For the right and bottom edges:
- *
- * a. Render the text to pixels using Terminus font.
- *
- * b. Blit the image on top of the text, using alpha channel.
- */
- int cellColumns = image.getWidth() / textWidth;
- if (cellColumns * textWidth < image.getWidth()) {
- cellColumns++;
- }
- int cellRows = image.getHeight() / textHeight;
- if (cellRows * textHeight < image.getHeight()) {
- cellRows++;
- }
-
- // Break the image up into an array of cells.
- Cell [][] cells = new Cell[cellColumns][cellRows];
-
- for (int x = 0; x < cellColumns; x++) {
- for (int y = 0; y < cellRows; y++) {
-
- int width = textWidth;
- if ((x + 1) * textWidth > image.getWidth()) {
- width = image.getWidth() - (x * textWidth);
- }
- int height = textHeight;
- if ((y + 1) * textHeight > image.getHeight()) {
- height = image.getHeight() - (y * textHeight);
- }
-
- Cell cell = new Cell();
- cell.setImage(image.getSubimage(x * textWidth,
- y * textHeight, width, height));
-
- cells[x][y] = cell;
- }
- }
-
- int x0 = currentState.cursorX;
- for (int y = 0; y < cellRows; y++) {
- for (int x = 0; x < cellColumns; x++) {
- assert (currentState.cursorX <= rightMargin);
-
- // TODO: Render text of current cell first, then image over
- // it (accounting for blank pixels). For now, just copy the
- // cell.
- DisplayLine line = display.get(currentState.cursorY);
- line.replace(currentState.cursorX, cells[x][y]);
-
- // If at the end of the visible screen, stop.
- if (currentState.cursorX == rightMargin) {
- break;
- }
- // Room for more image on the visible screen.
- currentState.cursorX++;
- }
- linefeed();
- cursorPosition(currentState.cursorY, x0);
+ if ((image.getWidth() < 1)
+ || (image.getWidth() > 10000)
+ || (image.getHeight() < 1)
+ || (image.getHeight() > 10000)
+ ) {
+ return;
}
+ imageToCells(image, true);
}
/**
- * Parse a "Jexer" image string into a bitmap image, and overlay that
+ * Parse a "Jexer" RGB image string into a bitmap image, and overlay that
* image onto the text cells.
*
* @param pw width token
* @param ps scroll token
* @param data pixel data
*/
- private void parseJexerImage(final String pw, final String ph,
+ private void parseJexerImageRGB(final String pw, final String ph,
final String ps, final String data) {
int imageWidth = 0;
return;
}
- java.util.Base64.Decoder base64 = java.util.Base64.getDecoder();
- byte [] bytes = base64.decode(data);
+ byte [] bytes = StringUtils.fromBase64(data.getBytes());
if (bytes.length != (imageWidth * imageHeight * 3)) {
return;
}
}
}
+ imageToCells(image, scroll);
+ }
+
+ /**
+ * Parse a "Jexer" PNG or JPG image string into a bitmap image, and
+ * overlay that image onto the text cells.
+ *
+ * @param type 1 for PNG, 2 for JPG
+ * @param ps scroll token
+ * @param data pixel data
+ */
+ private void parseJexerImageFile(final int type, final String ps,
+ final String data) {
+
+ int imageWidth = 0;
+ int imageHeight = 0;
+ boolean scroll = false;
+ BufferedImage image = null;
+ try {
+ byte [] bytes = StringUtils.fromBase64(data.getBytes());
+
+ switch (type) {
+ case 1:
+ if ((bytes[0] != (byte) 0x89)
+ || (bytes[1] != 'P')
+ || (bytes[2] != 'N')
+ || (bytes[3] != 'G')
+ || (bytes[4] != (byte) 0x0D)
+ || (bytes[5] != (byte) 0x0A)
+ || (bytes[6] != (byte) 0x1A)
+ || (bytes[7] != (byte) 0x0A)
+ ) {
+ // File does not have PNG header, bail out.
+ return;
+ }
+ break;
+
+ case 2:
+ if ((bytes[0] != (byte) 0XFF)
+ || (bytes[1] != (byte) 0xD8)
+ || (bytes[2] != (byte) 0xFF)
+ ) {
+ // File does not have JPG header, bail out.
+ return;
+ }
+ break;
+
+ default:
+ // Unsupported type, bail out.
+ return;
+ }
+
+ image = ImageIO.read(new ByteArrayInputStream(bytes));
+ } catch (IOException e) {
+ // SQUASH
+ return;
+ }
+ assert (image != null);
+ imageWidth = image.getWidth();
+ imageHeight = image.getHeight();
+ if ((imageWidth < 1)
+ || (imageWidth > 10000)
+ || (imageHeight < 1)
+ || (imageHeight > 10000)
+ ) {
+ return;
+ }
+ if (ps.equals("1")) {
+ scroll = true;
+ } else if (ps.equals("0")) {
+ scroll = false;
+ } else {
+ return;
+ }
+
+ imageToCells(image, scroll);
+ }
+
+ /**
+ * Break up an image into the cells at the current cursor.
+ *
+ * @param image the image to display
+ * @param scroll if true, scroll the image and move the cursor
+ */
+ private void imageToCells(final BufferedImage image, final boolean scroll) {
+ assert (image != null);
+
/*
* Procedure:
*
}
Cell cell = new Cell();
- cell.setImage(image.getSubimage(x * textWidth,
- y * textHeight, width, height));
+ if ((width != textWidth) || (height != textHeight)) {
+ BufferedImage newImage;
+ newImage = new BufferedImage(textWidth, textHeight,
+ BufferedImage.TYPE_INT_ARGB);
+
+ Graphics gr = newImage.getGraphics();
+ gr.drawImage(image.getSubimage(x * textWidth,
+ y * textHeight, width, height),
+ 0, 0, null, null);
+ gr.dispose();
+ cell.setImage(newImage);
+ } else {
+ cell.setImage(image.getSubimage(x * textWidth,
+ y * textHeight, width, height));
+ }
cells[x][y] = cell;
}
}
int x0 = currentState.cursorX;
+ int y0 = currentState.cursorY;
for (int y = 0; y < cellRows; y++) {
for (int x = 0; x < cellColumns; x++) {
assert (currentState.cursorX <= rightMargin);
+
+ // A real sixel terminal would render the text of the current
+ // cell first, then image over it (accounting for blank
+ // pixels). We do not support that. A cell is either text,
+ // or image, but not a mix of image-over-text.
DisplayLine line = display.get(currentState.cursorY);
line.replace(currentState.cursorX, cells[x][y]);
+
// If at the end of the visible screen, stop.
if (currentState.cursorX == rightMargin) {
break;
// Room for more image on the visible screen.
currentState.cursorX++;
}
- if ((scroll == true)
- || ((scroll == false)
- && (currentState.cursorY < scrollRegionBottom))
- ) {
+ if (currentState.cursorY < scrollRegionBottom - 1) {
+ // Not at the bottom, down a line.
linefeed();
+ } else if (scroll == true) {
+ // At the bottom, scroll as needed.
+ linefeed();
+ } else {
+ // At the bottom, no more scrolling, done.
+ break;
}
+
cursorPosition(currentState.cursorY, x0);
}
+ if (scroll == false) {
+ cursorPosition(y0, x0);
+ }
+
}
}
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
-import java.util.ArrayList;
import java.util.HashMap;
/**
case REPEAT:
if ((ch >= '0') && (ch <= '9')) {
if (repeatCount == -1) {
- repeatCount = (int) (ch - '0');
+ repeatCount = (ch - '0');
} else {
repeatCount *= 10;
- repeatCount += (int) (ch - '0');
+ repeatCount += (ch - '0');
}
}
return;
// TScrollableWidget ------------------------------------------------------
// ------------------------------------------------------------------------
+ /**
+ * Override TWidget's width: we need to set child widget widths.
+ *
+ * @param width new widget width
+ */
+ @Override
+ public void setWidth(final int width) {
+ super.setWidth(width);
+ if (hScroller != null) {
+ hScroller.setWidth(getWidth() - 1);
+ }
+ if (vScroller != null) {
+ vScroller.setX(getWidth() - 1);
+ }
+ if (treeView != null) {
+ treeView.setWidth(getWidth() - 1);
+ }
+ reflowData();
+ }
+
+ /**
+ * Override TWidget's height: we need to set child widget heights.
+ *
+ * @param height new widget height
+ */
+ @Override
+ public void setHeight(final int height) {
+ super.setHeight(height);
+ if (hScroller != null) {
+ hScroller.setY(getHeight() - 1);
+ }
+ if (vScroller != null) {
+ vScroller.setHeight(getHeight() - 1);
+ }
+ if (treeView != null) {
+ treeView.setHeight(getHeight() - 1);
+ }
+ reflowData();
+ }
+
/**
* Resize text and scrollbars for a new width/height.
*/
@Override
public void reflowData() {
+ if (treeView == null) {
+ return;
+ }
+
int selectedRow = 0;
boolean foundSelectedRow = false;